diff foosdk/sdk/foobar2000/foo_sample/listcontrol-advanced.cpp @ 1:20d02a178406 default tip

*: check in everything else yay
author Paper <paper@tflc.us>
date Mon, 05 Jan 2026 02:15:46 -0500
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/foosdk/sdk/foobar2000/foo_sample/listcontrol-advanced.cpp	Mon Jan 05 02:15:46 2026 -0500
@@ -0,0 +1,330 @@
+// Advanced CListControl use demo
+// Subclasses a CListControl to use all its features
+
+#include "stdafx.h"
+#include "resource.h"
+#include <helpers/atl-misc.h>
+#include <libPPUI/CListControlComplete.h>
+#include <libPPUI/CListControl-Cells.h>
+#include <string>
+#include <algorithm>
+#include <vector>
+
+#include <helpers/DarkMode.h>
+
+namespace {
+	struct listData_t {
+		std::string m_key, m_value;
+		bool m_checkState = false;
+	};
+	static std::vector<listData_t> makeListData() {
+		std::vector<listData_t> data;
+		data.resize( 10 );
+		for( size_t walk = 0; walk < data.size(); ++ walk ) {
+			auto & rec = data[walk];
+			rec.m_key = (PFC_string_formatter() << "Item #" << (walk+1) ).c_str();
+			rec.m_value = "edit me";
+		}
+		return data;
+	}
+	
+	// See CListControlComplete.h for base class description
+	typedef CListControlComplete CListControlDemoParent;
+
+	class CListControlDemo : public CListControlDemoParent {
+		typedef CListControlDemoParent parent_t;
+	public:
+		BEGIN_MSG_MAP_EX(CListControlDemo)
+			CHAIN_MSG_MAP( parent_t );
+			MSG_WM_CREATE(OnCreate)
+			MSG_WM_CONTEXTMENU(OnContextMenu)
+		END_MSG_MAP()
+
+
+		// Context menu handler
+		void OnContextMenu(CWindow wnd, CPoint point) {
+			// did we get a (-1,-1) point due to context menu key rather than right click?
+			// GetContextMenuPoint fixes that, returning a proper point at which the menu should be shown
+			point = this->GetContextMenuPoint(point);
+
+			CMenu menu; 
+			// WIN32_OP_D() : debug build only return value check
+			// Used to check for obscure errors in debug builds, does nothing (ignores errors) in release build
+			WIN32_OP_D(menu.CreatePopupMenu());
+
+			enum { ID_TEST1 = 1, ID_TEST2, ID_SELECTALL, ID_SELECTNONE, ID_INVERTSEL };
+			menu.AppendMenu(MF_STRING, ID_TEST1, L"Test 1");
+			menu.AppendMenu(MF_STRING, ID_TEST2, L"Test 2");
+			menu.AppendMenu(MF_SEPARATOR);
+			// Note: Ctrl+A handled automatically by CListControl, no need for us to catch it
+			menu.AppendMenu(MF_STRING, ID_SELECTALL, L"Select all\tCtrl+A");
+			menu.AppendMenu(MF_STRING, ID_SELECTNONE, L"Select none");
+			menu.AppendMenu(MF_STRING, ID_INVERTSEL, L"Invert selection");
+
+			int cmd;
+			{
+				// Callback object to show menu command descriptions in the status bar.
+				// it's actually a hidden window, needs a parent HWND, where we feed our control's HWND
+				CMenuDescriptionMap descriptions(m_hWnd);
+
+				// Set descriptions of all our items
+				descriptions.Set(ID_TEST1, "This is a test item #1");
+				descriptions.Set(ID_TEST2, "This is a test item #2");
+
+				descriptions.Set(ID_SELECTALL, "Selects all items");
+				descriptions.Set(ID_SELECTNONE, "Deselects all items");
+				descriptions.Set(ID_INVERTSEL, "Invert selection");
+
+				cmd = menu.TrackPopupMenuEx(TPM_RIGHTBUTTON | TPM_NONOTIFY | TPM_RETURNCMD, point.x, point.y, descriptions, nullptr);
+			}
+			switch(cmd) {
+			case ID_TEST1:
+			{
+				pfc::string_formatter msg;
+				msg << "Test command #1 triggered.\r\n";
+				msg << this->GetSelectedCount() << " items selected.";
+				// popup_message : non-blocking MessageBox equivalent
+				popup_message::g_show(msg, "Information");
+			}
+				break;
+			case ID_TEST2:
+			{
+				pfc::string_formatter msg;
+				msg << "Test command #1 triggered.\r\n";
+				msg << "Selected items:\r\n";
+				for( size_t walk = 0; walk < GetItemCount(); ++ walk) {
+					if ( this->IsItemSelected( walk ) ) {
+						msg << m_data[walk].m_key.c_str() << "\r\n";
+					}
+				}
+				msg << "End selected items.";
+				// popup_message : non-blocking MessageBox equivalent
+				popup_message::g_show(msg, "Information");
+			}
+				break;
+			case ID_SELECTALL:
+				this->SelectAll(); // trivial
+				break;
+			case ID_SELECTNONE:
+				this->SelectNone(); // trivial
+				break;
+			case ID_INVERTSEL:
+			{
+				auto mask = this->GetSelectionMask();
+				this->SetSelection( 
+					// Items which we alter - all of them
+					pfc::bit_array_true(),
+					// Selection values - NOT'd original selection mask
+					pfc::bit_array_not(mask) 
+				);
+				// Exclusion of footer item from selection handled via CanSelectItem()
+			}
+				break;
+			}
+		}
+		
+		int OnCreate(LPCREATESTRUCT lpCreateStruct) {
+			InitHeader(); // set up header control with columns
+			SetWindowText(L"List Control Demo"); // screen reader will see this
+			return 0;
+		}
+		void InitHeader() {
+			InitializeHeaderCtrl(HDS_FULLDRAG);
+
+			// never hardcode values in pixels, always use screen DPI
+			auto DPI = m_dpi;
+			AddColumn( "Check", MulDiv(60, DPI.cx, 96 ) );
+			AddColumn( "Name", MulDiv(100, DPI.cx, 96 ) );
+			AddColumn( "Value", MulDiv(100, DPI.cx, 96 ) );
+		}
+
+		bool CanSelectItem( size_t row ) const override {
+			// can not select footer
+			return row != footerRow();
+		}
+		size_t footerRow() const {
+			return m_data.size();
+		}
+		t_size GetItemCount() const override {
+			return m_data.size() + 1; // footer
+		}
+		void onFooterClicked() {
+			SelectNone();
+			listData_t obj = {};
+			obj.m_key = "New item";
+			size_t index = m_data.size();
+			m_data.push_back( std::move(obj) );
+			OnItemsInserted(index, 1, true);
+		}
+		void OnSubItemClicked(t_size item, t_size subItem, CPoint pt) override {
+			if ( item == footerRow() ) {
+				onFooterClicked(); return;
+			}
+			if ( subItem == 2 ) {
+				TableEdit_Start(item, subItem); return;
+			}
+			__super::OnSubItemClicked( item, subItem, pt );
+		}
+		bool AllowScrollbar(bool vertical) const override {
+			return true;
+		}
+		t_size InsertIndexFromPointEx(const CPoint & pt, bool & bInside) const override {
+			// Drag&drop insertion point hook, for reordering only
+			auto ret = __super::InsertIndexFromPointEx(pt, bInside);
+			bInside = false; // never drop *into* an item, only between, as we only allow reorder
+			if ( ret > m_data.size() ) ret = m_data.size(); // never allow drop beyond footer
+			return ret;
+		}
+		void RequestReorder(size_t const * order, size_t count) override {
+			// we've been asked to reorder the items, by either drag&drop or cursors+modifiers
+			// we can either reorder as requested, reorder partially if some of the items aren't moveable, or reject the request
+			
+			PFC_ASSERT( count == GetItemCount() );
+
+			// Footer row cannot be moved
+			if ( order[footerRow()] != footerRow() ) return;
+
+			pfc::reorder_t( m_data, order, count );
+			this->OnItemsReordered( order, count );
+		}
+		void RemoveMask( pfc::bit_array const & mask ) {
+			if ( mask.get(footerRow() ) ) return; // footer row cannot be removed
+			auto oldCount = GetItemCount();
+			pfc::remove_mask_t( m_data, mask );
+			this->OnItemsRemoved( mask, oldCount );
+		}
+		void RequestRemoveSelection() override {
+			// Delete key etc
+			RemoveMask(GetSelectionMask());
+		}
+		void ExecuteDefaultAction(t_size index) override {
+			// double click, enter key, etc
+			if ( index == footerRow() ) onFooterClicked();
+		}
+
+		bool GetSubItemText(t_size item, t_size subItem, pfc::string_base & out) const override {
+			if ( item == footerRow() ) {
+				if ( subItem == 0 ) {
+					out = "+ add new";
+					return true;
+				}
+				return false;
+			}
+			auto & rec = m_data[item];
+			switch(subItem) {
+			case 0:
+				// pass blank string or return false to create a checkbox only column
+				out = "check";
+				return true;
+			case 1:
+				out = rec.m_key.c_str();
+				return true;
+			case 2:
+				out = rec.m_value.c_str();
+				return true;
+			default:
+				return false;
+			}
+		}
+
+		size_t GetSubItemSpan(size_t row, size_t column) const override {
+			if ( row == footerRow() && column == 0 ) {
+				return GetColumnCount();				
+			}
+			return 1;
+		}
+		cellType_t GetCellType(size_t item, size_t subItem) const override {
+			// cellType_t is a pointer to a cell class object supplying cell behavior specification & rendering methods
+			// use PFC_SINGLETON to provide static instances of used cells
+			if ( item == footerRow() ) {
+				if ( subItem == 0 ) {
+					return & PFC_SINGLETON( CListCell_Button );
+				} else {
+					return nullptr;
+				}
+			}
+			switch(subItem) {
+			case 0:
+				return & PFC_SINGLETON( CListCell_Checkbox );
+			default:
+				return & PFC_SINGLETON( CListCell_Text );
+			}
+
+		}
+		bool GetCellTypeSupported() const override {
+			return true;
+		}
+		bool GetCellCheckState(size_t item, size_t subItem) const override {
+			if ( subItem == 0 ) {
+				auto & rec = m_data[item];
+				return rec.m_checkState;
+			}
+			return false;
+		}
+		void SetCellCheckState(size_t item, size_t subItem, bool value) override {
+			if ( subItem == 0 ) {
+				auto & rec = m_data[item];
+				if (rec.m_checkState != value) {
+					rec.m_checkState = value;
+					__super::SetCellCheckState(item, subItem, value);
+				}
+			}
+		}
+
+		uint32_t QueryDragDropTypes() const override {return dragDrop_reorder;}
+
+		// Inplace edit handlers
+		// Overrides of CTableEditHelperV2 methods
+		void TableEdit_SetField(t_size item, t_size subItem, const char * value) override {
+			if ( subItem == 2 ) {
+				m_data[item].m_value = value;
+				ReloadItem( item );
+			}
+		}
+		bool TableEdit_IsColumnEditable(t_size subItem) const override {
+			return subItem == 2; 
+		}
+	private:
+		std::vector< listData_t > m_data = makeListData();
+	};
+
+	// Straightforward WTL dialog code
+	class CListControlAdvancedDemoDialog : public CDialogImpl<CListControlAdvancedDemoDialog> {
+	public:
+		enum { IDD = IDD_LISTCONTROL_DEMO };
+
+		BEGIN_MSG_MAP_EX(CListControlAdvancedDemoDialog)
+			MSG_WM_INITDIALOG(OnInitDialog)
+			COMMAND_HANDLER_EX(IDCANCEL, BN_CLICKED, OnCancel)
+		END_MSG_MAP()
+	private:
+		void OnCancel(UINT, int, CWindow) {
+			DestroyWindow();
+		}
+
+		BOOL OnInitDialog(CWindow, LPARAM) {
+
+			// Create replacing existing windows list control
+			// automatically initialize position, font, etc
+			m_list.CreateInDialog( *this, IDC_LIST1 );
+
+			// Do this AFTER creating CListControl, so dark mode hook talks to new CListControl rather than shortlived IDC_LIST1 placeholder
+			m_dark.AddDialogWithControls(*this);
+
+			ShowWindow(SW_SHOW);
+
+			return TRUE; // system should set focus
+		}
+
+		CListControlDemo m_list;
+
+		fb2k::CDarkModeHooks m_dark;
+	};
+}
+
+// Called from mainmenu.cpp
+void RunListControlAdvancedDemo() {
+	// automatically creates the dialog with object lifetime management and modeless dialog registration
+	fb2k::newDialog<CListControlAdvancedDemoDialog>();	
+}
+