latest SDK
This commit is contained in:
329
foobar2000/foo_sample/IO.cpp
Normal file
329
foobar2000/foo_sample/IO.cpp
Normal file
@@ -0,0 +1,329 @@
|
||||
#include "stdafx.h"
|
||||
#include <helpers/file_list_helper.h>
|
||||
#include <helpers/filetimetools.h>
|
||||
|
||||
void RunIOTest() {
|
||||
try {
|
||||
abort_callback_dummy noAbort;
|
||||
auto request = http_client::get()->create_request("GET");
|
||||
request->run("https://www.foobar2000.org", noAbort);
|
||||
} catch (std::exception const & e) {
|
||||
popup_message::g_show( PFC_string_formatter() << "Network test failure:\n" << e, "Information");
|
||||
return;
|
||||
}
|
||||
popup_message::g_show(PFC_string_formatter() << "Network test OK", "Information");
|
||||
}
|
||||
|
||||
namespace { // anon namespace local classes for good measure
|
||||
class tpc_copyFiles : public threaded_process_callback {
|
||||
public:
|
||||
tpc_copyFiles ( metadb_handle_list_cref items, const char * pathTo ) : m_pathTo(pathTo), m_outFS(filesystem::get(pathTo)) {
|
||||
m_lstFiles.init_from_list( items );
|
||||
}
|
||||
|
||||
void on_init(HWND p_wnd) override {
|
||||
// Main thread, called before run() gets started
|
||||
}
|
||||
void run(threaded_process_status & p_status, abort_callback & p_abort) override {
|
||||
// Worker thread
|
||||
for( size_t fileWalk = 0; fileWalk < m_lstFiles.get_size(); ++ fileWalk ) {
|
||||
|
||||
// always do this in every timeconsuming loop
|
||||
// will throw exception_aborted if the user pushed 'cancel' on us
|
||||
p_abort.check();
|
||||
|
||||
const char * inPath = m_lstFiles[fileWalk];
|
||||
p_status.set_progress(fileWalk, m_lstFiles.get_size());
|
||||
p_status.set_item_path( inPath );
|
||||
|
||||
try {
|
||||
workWithFile(inPath, p_abort);
|
||||
} catch(exception_aborted) {
|
||||
// User abort, just bail
|
||||
throw;
|
||||
} catch(std::exception const & e) {
|
||||
m_errorLog << "Could not copy: " << file_path_display(inPath) << ", reason: " << e << "\n";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void workWithFile( const char * inPath, abort_callback & abort ) {
|
||||
FB2K_console_formatter() << "File: " << file_path_display(inPath);
|
||||
|
||||
// Filesystem API for inPath
|
||||
const filesystem::ptr inFS = filesystem::get( inPath );
|
||||
pfc::string8 inFN;
|
||||
// Extract filename+extension according to this filesystem's rules
|
||||
// If it's HTTP or so, there might be ?key=value that needs stripping
|
||||
inFS->extract_filename_ext(inPath, inFN);
|
||||
|
||||
pfc::string8 outPath = m_pathTo;
|
||||
// Suffix with outFS path separator. On Windows local filesystem this is always a backslash.
|
||||
outPath.end_with( m_outFS->pathSeparator() );
|
||||
outPath += inFN;
|
||||
|
||||
const double openTimeout = 1.0; // wait and keep retrying for up to 1 second in case of a sharing violation error
|
||||
|
||||
// Not strictly needed, but we do it anyway
|
||||
// Acquire a read lock on the file, so anyone trying to acquire a write lock will just wait till we have finished
|
||||
auto inLock = file_lock_manager::get()->acquire_read( inPath, abort );
|
||||
|
||||
auto inFile = inFS->openRead( inPath, abort, openTimeout );
|
||||
|
||||
// Let's toy around with inFile
|
||||
{
|
||||
auto stats = inFile->get_stats(abort);
|
||||
|
||||
if ( stats.m_size != filesize_invalid ) {
|
||||
FB2K_console_formatter() << "Size: " << pfc::format_file_size_short(stats.m_size);
|
||||
}
|
||||
if ( stats.m_timestamp != filetimestamp_invalid ) {
|
||||
FB2K_console_formatter() << "Last modified: " << format_filetimestamp(stats.m_timestamp);
|
||||
}
|
||||
pfc::string8 contentType;
|
||||
if ( inFile->get_content_type( contentType ) ) {
|
||||
FB2K_console_formatter() << "Content type: " << contentType;
|
||||
}
|
||||
|
||||
uint8_t buffer[256];
|
||||
size_t got = inFile->read(buffer, sizeof(buffer), abort);
|
||||
|
||||
if ( got > 0 ) {
|
||||
FB2K_console_formatter() << "Header bytes: " << pfc::format_hexdump( buffer, got );
|
||||
}
|
||||
|
||||
if ( inFile->is_remote() ) {
|
||||
FB2K_console_formatter() << "File is remote";
|
||||
} else {
|
||||
FB2K_console_formatter() << "File is local";
|
||||
}
|
||||
|
||||
// For seekable files, reopen() seeks to the beginning.
|
||||
// For nonseekable stream, reopen() restarts reading the stream.
|
||||
inFile->reopen(abort);
|
||||
}
|
||||
|
||||
// This is a glorified strcmp() for file paths.
|
||||
if ( metadb::path_compare( inPath, outPath ) == 0 ) {
|
||||
// Same path, go no further. Specifically don't attempt acquiring a writelock because that will never complete, unless user aborted.
|
||||
FB2K_console_formatter() << "Input and output paths are the same - not copying!";
|
||||
return;
|
||||
}
|
||||
|
||||
// Required to write to files being currently played.
|
||||
// See file_lock_manager documentation for details.
|
||||
auto outLock = file_lock_manager::get()->acquire_write( outPath, abort );
|
||||
|
||||
// WARNING : if a file exists at outPath prior to this, it will be reset to zero bytes (win32 CREATE_ALWAYS semantics)
|
||||
auto outFile = m_outFS->openWriteNew(outPath, abort, openTimeout);
|
||||
|
||||
|
||||
|
||||
try {
|
||||
// Refer to g_transfer_file implementation details in the SDK for lowlevel reading & writing details
|
||||
file::g_transfer_file(inFile, outFile, abort);
|
||||
} catch(...) {
|
||||
|
||||
if ( inFile->is_remote() ) {
|
||||
// Remote file was being downloaded? Suppress deletion of incomplete output
|
||||
throw;
|
||||
}
|
||||
// Failed for some reason
|
||||
// Release our destination file hadnle
|
||||
outFile.release();
|
||||
|
||||
// .. and delete the incomplete file
|
||||
try {
|
||||
abort_callback_dummy noAbort; // we might be being aborted, don't let that prevent deletion
|
||||
m_outFS->remove( outPath, noAbort );
|
||||
} catch(...) {
|
||||
// disregard errors - just report original copy error
|
||||
}
|
||||
|
||||
throw; // rethrow the original copy error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void on_done(HWND p_wnd, bool p_was_aborted) override {
|
||||
// All done, main thread again
|
||||
|
||||
if (! p_was_aborted && m_errorLog.length() > 0 ) {
|
||||
popup_message::g_show(m_errorLog, "Information");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// This is a helper class that generates a sorted list of unique file paths in this metadb_handle_list.
|
||||
// The metadb_handle_list might contain duplicate tracks or multiple subsongs in the same file. m_lstFiles will list each file only once.
|
||||
file_list_helper::file_list_from_metadb_handle_list m_lstFiles;
|
||||
|
||||
// Destination path
|
||||
const pfc::string8 m_pathTo;
|
||||
// Destination filesystem API. Obtained via filesystem::get() with destination path.
|
||||
const filesystem::ptr m_outFS;
|
||||
|
||||
// Error log
|
||||
pfc::string_formatter m_errorLog;
|
||||
};
|
||||
}
|
||||
|
||||
void RunCopyFiles(metadb_handle_list_cref data) {
|
||||
|
||||
// Detect modal dialog wars.
|
||||
// If another modal dialog is active, bump it instead of allowing our modal dialog (uBrowseForFolder) to run.
|
||||
// Suppress this if the relevant code is intended to be launched by a modal dialog.
|
||||
if (!ModalDialogPrologue()) return;
|
||||
|
||||
const HWND wndParent = core_api::get_main_window();
|
||||
pfc::string8 copyTo;
|
||||
// shared.dll method
|
||||
if (!uBrowseForFolder( wndParent, "Choose destination folder", copyTo )) return;
|
||||
|
||||
// shared.dll methods are win32 API wrappers and return plain paths with no protocol prepended
|
||||
// Prefix with file:// before passing to fb2k filesystem methods.
|
||||
// Actually the standard fb2k filesystem implementation recognizes paths even without the prefix, but we enforce it here as a good practice.
|
||||
pfc::string8 copyTo2 = PFC_string_formatter() << "file://" << copyTo;
|
||||
|
||||
// Create worker object, a threaded_process_callback implementation.
|
||||
auto worker = fb2k::service_new<tpc_copyFiles>(data, copyTo2);
|
||||
const uint32_t flags = threaded_process::flag_show_abort | threaded_process::flag_show_progress | threaded_process::flag_show_item;
|
||||
// Start the process asynchronously.
|
||||
threaded_process::get()->run_modeless( worker, flags, wndParent, "Sample Component: Copying Files" );
|
||||
|
||||
// Our worker is now running.
|
||||
}
|
||||
|
||||
|
||||
namespace {
|
||||
class processLLtags : public threaded_process_callback {
|
||||
public:
|
||||
processLLtags( metadb_handle_list_cref data ) : m_items(data) {
|
||||
m_hints = metadb_io_v2::get()->create_hint_list();
|
||||
}
|
||||
void on_init(ctx_t p_wnd) override {
|
||||
// Main thread, called before run() gets started
|
||||
}
|
||||
void run(threaded_process_status & p_status, abort_callback & p_abort) override {
|
||||
// Worker thread
|
||||
|
||||
// Note:
|
||||
// We should look for references to the same file (such as multiple subsongs) in the track list and update each file only once.
|
||||
// But for the sake of simplicity we don't do this in a sample component.
|
||||
for( size_t itemWalk = 0; itemWalk < m_items.get_size(); ++ itemWalk ) {
|
||||
|
||||
// always do this in every timeconsuming loop
|
||||
// will throw exception_aborted if the user pushed 'cancel' on us
|
||||
p_abort.check();
|
||||
|
||||
auto item = m_items[ itemWalk ];
|
||||
p_status.set_progress( itemWalk, m_items.get_size() );
|
||||
p_status.set_item_path( item->get_path() );
|
||||
|
||||
try {
|
||||
workWithTrack(item, p_abort);
|
||||
} catch(exception_aborted) {
|
||||
// User abort, just bail
|
||||
throw;
|
||||
} catch(std::exception const & e) {
|
||||
m_errorLog << "Could not update: " << item << ", reason: " << e << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
void on_done(ctx_t p_wnd, bool p_was_aborted) override {
|
||||
// All done, main thread again
|
||||
if ( m_hints.is_valid() ) {
|
||||
// This is the proper time to finalize the hint list
|
||||
// All playlists showing these files and such will now be refreshed
|
||||
m_hints->on_done();
|
||||
}
|
||||
|
||||
if (!p_was_aborted && m_errorLog.length() > 0) {
|
||||
popup_message::g_show(m_errorLog, "Information");
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void workWithTrack( metadb_handle_ptr item, abort_callback & abort ) {
|
||||
|
||||
FB2K_console_formatter() << "foo_sample will update tags on: " << item;
|
||||
|
||||
const auto subsong = item->get_subsong_index();
|
||||
const auto path = item->get_path();
|
||||
|
||||
const double openTimeout = 1.0; // wait and keep retrying for up to 1 second in case of a sharing violation error
|
||||
|
||||
// Required to write to files being currently played.
|
||||
// See file_lock_manager documentation for details.
|
||||
auto lock = file_lock_manager::get()->acquire_write( path, abort );
|
||||
|
||||
input_info_writer::ptr writer;
|
||||
input_entry::g_open_for_info_write_timeout(writer, nullptr, path, abort, openTimeout );
|
||||
|
||||
{ // let's toy around with info that the writer can hand to us
|
||||
auto stats = writer->get_file_stats( abort );
|
||||
if (stats.m_timestamp != filetimestamp_invalid) {
|
||||
FB2K_console_formatter() << "Last-modified before tag update: " << format_filetimestamp_utc(stats.m_timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
file_info_impl info;
|
||||
writer->get_info( subsong, info, abort );
|
||||
|
||||
info.meta_set("comment", "foo_sample lowlevel tags write demo was here");
|
||||
|
||||
// Touchy subject
|
||||
// Should we let the user abort an incomplete tag write?
|
||||
// Let's better not
|
||||
abort_callback_dummy noAbort;
|
||||
|
||||
// This can be called many times for files with multiple subsongs
|
||||
writer->set_info( subsong, info, noAbort );
|
||||
|
||||
// This is called once - when we're done set_info()'ing
|
||||
writer->commit( noAbort );
|
||||
|
||||
{ // let's toy around with info that the writer can hand to us
|
||||
auto stats = writer->get_file_stats(abort);
|
||||
if (stats.m_timestamp != filetimestamp_invalid) {
|
||||
FB2K_console_formatter() << "Last-modified after tag update: " << format_filetimestamp_utc(stats.m_timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
// Now send new info to metadb
|
||||
// If we don't do this, old info may still be shown in playlists etc.
|
||||
if ( true ) {
|
||||
// Method #1: feed altered info directly to the hintlist.
|
||||
// Makes sense here as we updated just one subsong.
|
||||
auto stats = writer->get_file_stats(abort);
|
||||
m_hints->add_hint(item, info, stats, true);
|
||||
} else {
|
||||
// Method #2: let metadb talk to our writer object (more commonly used).
|
||||
// The writer is a subclass of input_info_reader and can therefore be legitimately fed to add_hint_reader()
|
||||
// This will read info from all subsongs in the file and update metadb if appropriate.
|
||||
m_hints->add_hint_reader(path, writer, abort);
|
||||
}
|
||||
}
|
||||
metadb_hint_list::ptr m_hints;
|
||||
const metadb_handle_list m_items;
|
||||
pfc::string_formatter m_errorLog;
|
||||
};
|
||||
}
|
||||
void RunAlterTagsLL(metadb_handle_list_cref data) {
|
||||
|
||||
const HWND wndParent = core_api::get_main_window();
|
||||
|
||||
// Our worker object, a threaded_process_callback subclass.
|
||||
auto worker = fb2k::service_new< processLLtags > ( data );
|
||||
|
||||
const uint32_t flags = threaded_process::flag_show_abort | threaded_process::flag_show_progress | threaded_process::flag_show_item;
|
||||
|
||||
// Start the worker asynchronously.
|
||||
threaded_process::get()->run_modeless(worker, flags, wndParent, "Sample Component: Updating Tags");
|
||||
|
||||
// The worker is now running.
|
||||
|
||||
}
|
||||
3
foobar2000/foo_sample/PCH.cpp
Normal file
3
foobar2000/foo_sample/PCH.cpp
Normal file
@@ -0,0 +1,3 @@
|
||||
#include "stdafx.h"
|
||||
|
||||
// This is a dummy source code file that just generates the precompiled header (PCH) file for use when compiling the rest of the source code, to speed compilation up.
|
||||
174
foobar2000/foo_sample/contextmenu.cpp
Normal file
174
foobar2000/foo_sample/contextmenu.cpp
Normal file
@@ -0,0 +1,174 @@
|
||||
#include "stdafx.h"
|
||||
|
||||
|
||||
// Identifier of our context menu group. Substitute with your own when reusing code.
|
||||
static const GUID guid_mygroup = { 0x572de7f4, 0xcbdf, 0x479a, { 0x97, 0x26, 0xa, 0xb0, 0x97, 0x47, 0x69, 0xe3 } };
|
||||
|
||||
|
||||
// Switch to contextmenu_group_factory to embed your commands in the root menu but separated from other commands.
|
||||
|
||||
//static contextmenu_group_factory g_mygroup(guid_mygroup, contextmenu_groups::root, 0);
|
||||
static contextmenu_group_popup_factory g_mygroup(guid_mygroup, contextmenu_groups::root, "Sample component", 0);
|
||||
|
||||
static void RunTestCommand(metadb_handle_list_cref data);
|
||||
void RunCalculatePeak(metadb_handle_list_cref data); //decode.cpp
|
||||
|
||||
void RunCopyFiles(metadb_handle_list_cref data); // IO.cpp
|
||||
void RunAlterTagsLL(metadb_handle_list_cref data); // IO.cpp
|
||||
|
||||
void RunUIAndThreads( metadb_handle_list_cref data ); // ui_and_threads.cpp
|
||||
|
||||
namespace { // anon namespace local classes for good measure
|
||||
class myFilter : public file_info_filter {
|
||||
public:
|
||||
bool apply_filter(metadb_handle_ptr p_location, t_filestats p_stats, file_info & p_info) {
|
||||
p_info.meta_set("comment", "foo_sample was here");
|
||||
// return true to write changes tags to the file, false to suppress the update
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static void RunAlterTags(metadb_handle_list_cref data) {
|
||||
// Simple alter-file-tags functionality
|
||||
|
||||
const HWND wndParent = core_api::get_main_window();
|
||||
|
||||
// Filter object that applies our edits to the file tags
|
||||
auto filter = fb2k::service_new<myFilter>();
|
||||
|
||||
auto notify = fb2k::makeCompletionNotify( [] (unsigned code) {
|
||||
// Code values are metadb_io::t_update_info_state enum
|
||||
FB2K_console_formatter() << "Tag update finished, code: " << code;
|
||||
} );
|
||||
|
||||
// Flags
|
||||
// Indicate that we're aware of fb2k 1.3+ partial info semantics
|
||||
const uint32_t flags = metadb_io_v2::op_flag_partial_info_aware;
|
||||
|
||||
metadb_io_v2::get()->update_info_async(data, filter, wndParent, flags, notify);
|
||||
}
|
||||
|
||||
// Simple context menu item class.
|
||||
class myitem : public contextmenu_item_simple {
|
||||
public:
|
||||
enum {
|
||||
cmd_test1 = 0,
|
||||
cmd_peak,
|
||||
cmd_copyFiles,
|
||||
cmd_alterTags,
|
||||
cmd_alterTagsLL,
|
||||
cmd_uiAndThreads,
|
||||
cmd_total
|
||||
};
|
||||
GUID get_parent() {return guid_mygroup;}
|
||||
unsigned get_num_items() {return cmd_total;}
|
||||
void get_item_name(unsigned p_index,pfc::string_base & p_out) {
|
||||
switch(p_index) {
|
||||
case cmd_test1: p_out = "Test command"; break;
|
||||
case cmd_peak: p_out = "Calculate peak"; break;
|
||||
case cmd_copyFiles: p_out = "Copy files"; break;
|
||||
case cmd_alterTags: p_out = "Alter tags"; break;
|
||||
case cmd_alterTagsLL: p_out = "Alter tags (low level)"; break;
|
||||
case cmd_uiAndThreads: p_out = "UI and threads demo"; break;
|
||||
default: uBugCheck(); // should never happen unless somebody called us with invalid parameters - bail
|
||||
}
|
||||
}
|
||||
void context_command(unsigned p_index,metadb_handle_list_cref p_data,const GUID& p_caller) {
|
||||
switch(p_index) {
|
||||
case cmd_test1:
|
||||
RunTestCommand(p_data);
|
||||
break;
|
||||
case cmd_peak:
|
||||
RunCalculatePeak(p_data);
|
||||
break;
|
||||
case cmd_copyFiles:
|
||||
RunCopyFiles(p_data);
|
||||
break;
|
||||
case cmd_alterTags:
|
||||
RunAlterTags(p_data);
|
||||
break;
|
||||
case cmd_alterTagsLL:
|
||||
RunAlterTagsLL(p_data);
|
||||
break;
|
||||
case cmd_uiAndThreads:
|
||||
RunUIAndThreads(p_data);
|
||||
break;
|
||||
default:
|
||||
uBugCheck();
|
||||
}
|
||||
}
|
||||
// Overriding this is not mandatory. We're overriding it just to demonstrate stuff that you can do such as context-sensitive menu item labels.
|
||||
bool context_get_display(unsigned p_index,metadb_handle_list_cref p_data,pfc::string_base & p_out,unsigned & p_displayflags,const GUID & p_caller) {
|
||||
switch(p_index) {
|
||||
case cmd_test1:
|
||||
if (!__super::context_get_display(p_index, p_data, p_out, p_displayflags, p_caller)) return false;
|
||||
// Example context sensitive label: append the count of selected items to the label.
|
||||
p_out << " : " << p_data.get_count() << " item";
|
||||
if (p_data.get_count() != 1) p_out << "s";
|
||||
p_out << " selected";
|
||||
return true;
|
||||
default:
|
||||
return __super::context_get_display(p_index, p_data, p_out, p_displayflags, p_caller);
|
||||
}
|
||||
}
|
||||
GUID get_item_guid(unsigned p_index) {
|
||||
// These GUIDs identify our context menu items. Substitute with your own GUIDs when reusing code.
|
||||
static const GUID guid_test1 = { 0x4021c80d, 0x9340, 0x423b, { 0xa3, 0xe2, 0x8e, 0x1e, 0xda, 0x87, 0x13, 0x7f } };
|
||||
static const GUID guid_peak = { 0xe629b5c3, 0x5af3, 0x4a1e, { 0xa0, 0xcd, 0x2d, 0x5b, 0xff, 0xa6, 0x4, 0x58 } };
|
||||
static const GUID guid_copyFiles = { 0x7f8a6569, 0xe46b, 0x4698, { 0xaa, 0x30, 0xc4, 0xc1, 0x44, 0xc9, 0xc8, 0x92 } };
|
||||
static const GUID guid_alterTags = { 0xdfb8182b, 0xf8f3, 0x4ce9, { 0xae, 0xf6, 0x8e, 0x4e, 0x51, 0x7c, 0x2d, 0x3 } };
|
||||
static const GUID guid_alterTagsLL = { 0x6b43324d, 0x6cb2, 0x42a6, { 0xbf, 0xc, 0xd9, 0x43, 0xfc, 0x83, 0x2f, 0x39 } };
|
||||
static const GUID guid_uiAndThreads = { 0x30dace2e, 0xcccf, 0x41d4, { 0x8c, 0x24, 0x57, 0xec, 0xf4, 0xa0, 0xd9, 0xc9 } };
|
||||
|
||||
switch(p_index) {
|
||||
case cmd_test1: return guid_test1;
|
||||
case cmd_peak: return guid_peak;
|
||||
case cmd_copyFiles: return guid_copyFiles;
|
||||
case cmd_alterTags: return guid_alterTags;
|
||||
case cmd_alterTagsLL: return guid_alterTagsLL;
|
||||
case cmd_uiAndThreads: return guid_uiAndThreads;
|
||||
default: uBugCheck(); // should never happen unless somebody called us with invalid parameters - bail
|
||||
}
|
||||
|
||||
}
|
||||
bool get_item_description(unsigned p_index,pfc::string_base & p_out) {
|
||||
switch(p_index) {
|
||||
case cmd_test1:
|
||||
p_out = "This is a sample command.";
|
||||
return true;
|
||||
case cmd_peak:
|
||||
p_out = "This is a sample command that decodes the selected tracks and reports the peak sample value.";
|
||||
return true;
|
||||
case cmd_copyFiles:
|
||||
p_out = "This is a sample command that copies the selected tracks to another location.";
|
||||
return true;
|
||||
case cmd_alterTags:
|
||||
p_out = "This is a sample command that performs tag manipulation on the files.";
|
||||
return true;
|
||||
case cmd_alterTagsLL:
|
||||
p_out = "This is a sample command that performs low-level manipulation of tags on the files.";
|
||||
return true;
|
||||
case cmd_uiAndThreads:
|
||||
p_out = "This is a smple command that runs UI and Threads demo.";
|
||||
return true;
|
||||
default:
|
||||
uBugCheck(); // should never happen unless somebody called us with invalid parameters - bail
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static contextmenu_item_factory_t<myitem> g_myitem_factory;
|
||||
|
||||
|
||||
static void RunTestCommand(metadb_handle_list_cref data) {
|
||||
pfc::string_formatter message;
|
||||
message << "This is a test command.\n";
|
||||
if (data.get_count() > 0) {
|
||||
message << "Parameters:\n";
|
||||
for(t_size walk = 0; walk < data.get_count(); ++walk) {
|
||||
message << data[walk] << "\n";
|
||||
}
|
||||
}
|
||||
popup_message::g_show(message, "Blah");
|
||||
}
|
||||
77
foobar2000/foo_sample/decode.cpp
Normal file
77
foobar2000/foo_sample/decode.cpp
Normal file
@@ -0,0 +1,77 @@
|
||||
#include "stdafx.h"
|
||||
#include <helpers/input_helpers.h>
|
||||
|
||||
class calculate_peak_process : public threaded_process_callback {
|
||||
public:
|
||||
calculate_peak_process(metadb_handle_list_cref items) : m_items(items), m_peak() {}
|
||||
void on_init(HWND p_wnd) {}
|
||||
void run(threaded_process_status & p_status,abort_callback & p_abort) {
|
||||
try {
|
||||
const t_uint32 decode_flags = input_flag_no_seeking | input_flag_no_looping; // tell the decoders that we won't seek and that we don't want looping on formats that support looping.
|
||||
input_helper input; // this object manages lowlevel input_decoder calls for us.
|
||||
for(t_size walk = 0; walk < m_items.get_size(); ++walk) {
|
||||
p_abort.check(); // in case the input we're working with fails at doing this
|
||||
p_status.set_progress(walk, m_items.get_size());
|
||||
p_status.set_progress_secondary(0);
|
||||
p_status.set_item_path( m_items[walk]->get_path() );
|
||||
input.open(NULL, m_items[walk], decode_flags, p_abort);
|
||||
|
||||
double length;
|
||||
{ // fetch the track length for proper dual progress display;
|
||||
file_info_impl info;
|
||||
// input.open should have preloaded relevant info, no need to query the input itself again.
|
||||
// Regular get_info() may not retrieve freshly loaded info yet at this point (it will start giving the new info when relevant info change callbacks are dispatched); we need to use get_info_async.
|
||||
if (m_items[walk]->get_info_async(info)) length = info.get_length();
|
||||
else length = 0;
|
||||
}
|
||||
|
||||
audio_chunk_impl_temporary l_chunk;
|
||||
double decoded = 0;
|
||||
while(input.run(l_chunk, p_abort)) { // main decode loop
|
||||
m_peak = l_chunk.get_peak(m_peak);
|
||||
if (length > 0) { // don't bother for unknown length tracks
|
||||
decoded += l_chunk.get_duration();
|
||||
if (decoded > length) decoded = length;
|
||||
p_status.set_progress_secondary_float(decoded / length);
|
||||
}
|
||||
p_abort.check(); // in case the input we're working with fails at doing this
|
||||
}
|
||||
}
|
||||
} catch(std::exception const & e) {
|
||||
m_failMsg = e.what();
|
||||
}
|
||||
}
|
||||
void on_done(HWND p_wnd,bool p_was_aborted) {
|
||||
if (!p_was_aborted) {
|
||||
if (!m_failMsg.is_empty()) {
|
||||
popup_message::g_complain("Peak scan failure", m_failMsg);
|
||||
} else {
|
||||
pfc::string_formatter result;
|
||||
result << "Value: " << m_peak << "\n\n";
|
||||
result << "Scanned items:\n";
|
||||
for(t_size walk = 0; walk < m_items.get_size(); ++walk) {
|
||||
result << m_items[walk] << "\n";
|
||||
}
|
||||
popup_message::g_show(result,"Peak scan result");
|
||||
}
|
||||
}
|
||||
}
|
||||
private:
|
||||
audio_sample m_peak;
|
||||
pfc::string8 m_failMsg;
|
||||
const metadb_handle_list m_items;
|
||||
};
|
||||
|
||||
void RunCalculatePeak(metadb_handle_list_cref data) {
|
||||
try {
|
||||
if (data.get_count() == 0) throw pfc::exception_invalid_params();
|
||||
service_ptr_t<threaded_process_callback> cb = new service_impl_t<calculate_peak_process>(data);
|
||||
static_api_ptr_t<threaded_process>()->run_modeless(
|
||||
cb,
|
||||
threaded_process::flag_show_progress_dual | threaded_process::flag_show_item | threaded_process::flag_show_abort,
|
||||
core_api::get_main_window(),
|
||||
"Sample component: peak scan");
|
||||
} catch(std::exception const & e) {
|
||||
popup_message::g_complain("Could not start peak scan process", e);
|
||||
}
|
||||
}
|
||||
154
foobar2000/foo_sample/dsp.cpp
Normal file
154
foobar2000/foo_sample/dsp.cpp
Normal file
@@ -0,0 +1,154 @@
|
||||
#include "stdafx.h"
|
||||
#include "resource.h"
|
||||
|
||||
static void RunDSPConfigPopup(const dsp_preset & p_data,HWND p_parent,dsp_preset_edit_callback & p_callback);
|
||||
|
||||
class dsp_sample : public dsp_impl_base
|
||||
{
|
||||
public:
|
||||
dsp_sample(dsp_preset const & in) : m_gain(0) {
|
||||
parse_preset(m_gain, in);
|
||||
}
|
||||
|
||||
static GUID g_get_guid() {
|
||||
//This is our GUID. Generate your own one when reusing this code.
|
||||
static const GUID guid = { 0x890827b, 0x67df, 0x4c27, { 0xba, 0x1a, 0x4f, 0x95, 0x8d, 0xf, 0xb5, 0xd0 } };
|
||||
return guid;
|
||||
}
|
||||
|
||||
static void g_get_name(pfc::string_base & p_out) { p_out = "Sample DSP";}
|
||||
|
||||
bool on_chunk(audio_chunk * chunk,abort_callback &) {
|
||||
// Perform any operations on the chunk here.
|
||||
// The most simple DSPs can just alter the chunk in-place here and skip the following functions.
|
||||
|
||||
|
||||
// trivial DSP code: apply our gain to the audio data.
|
||||
chunk->scale( audio_math::gain_to_scale( m_gain ) );
|
||||
|
||||
// To retrieve the currently processed track, use get_cur_file().
|
||||
// Warning: the track is not always known - it's up to the calling component to provide this data and in some situations we'll be working with data that doesn't originate from an audio file.
|
||||
// If you rely on get_cur_file(), you should change need_track_change_mark() to return true to get accurate information when advancing between tracks.
|
||||
|
||||
return true; //Return true to keep the chunk or false to drop it from the chain.
|
||||
}
|
||||
|
||||
void on_endofplayback(abort_callback &) {
|
||||
// The end of playlist has been reached, we've already received the last decoded audio chunk.
|
||||
// We need to finish any pending processing and output any buffered data through insert_chunk().
|
||||
}
|
||||
void on_endoftrack(abort_callback &) {
|
||||
// Should do nothing except for special cases where your DSP performs special operations when changing tracks.
|
||||
// If this function does anything, you must change need_track_change_mark() to return true.
|
||||
// If you have pending audio data that you wish to output, you can use insert_chunk() to do so.
|
||||
}
|
||||
|
||||
void flush() {
|
||||
// If you have any audio data buffered, you should drop it immediately and reset the DSP to a freshly initialized state.
|
||||
// Called after a seek etc.
|
||||
}
|
||||
|
||||
double get_latency() {
|
||||
// If the DSP buffers some amount of audio data, it should return the duration of buffered data (in seconds) here.
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool need_track_change_mark() {
|
||||
// Return true if you need on_endoftrack() or need to accurately know which track we're currently processing
|
||||
// WARNING: If you return true, the DSP manager will fire on_endofplayback() at DSPs that are before us in the chain on track change to ensure that we get an accurate mark, so use it only when needed.
|
||||
return false;
|
||||
}
|
||||
static bool g_get_default_preset(dsp_preset & p_out) {
|
||||
make_preset(0, p_out);
|
||||
return true;
|
||||
}
|
||||
static void g_show_config_popup(const dsp_preset & p_data,HWND p_parent,dsp_preset_edit_callback & p_callback) {
|
||||
::RunDSPConfigPopup(p_data, p_parent, p_callback);
|
||||
}
|
||||
static bool g_have_config_popup() {return true;}
|
||||
static void make_preset(float gain, dsp_preset & out) {
|
||||
dsp_preset_builder builder; builder << gain; builder.finish(g_get_guid(), out);
|
||||
}
|
||||
static void parse_preset(float & gain, const dsp_preset & in) {
|
||||
try {
|
||||
dsp_preset_parser parser(in); parser >> gain;
|
||||
} catch(exception_io_data) {gain = 0;}
|
||||
}
|
||||
private:
|
||||
float m_gain;
|
||||
};
|
||||
|
||||
// Use dsp_factory_nopreset_t<> instead of dsp_factory_t<> if your DSP does not provide preset/configuration functionality.
|
||||
static dsp_factory_t<dsp_sample> g_dsp_sample_factory;
|
||||
|
||||
|
||||
class CMyDSPPopup : public CDialogImpl<CMyDSPPopup> {
|
||||
public:
|
||||
CMyDSPPopup(const dsp_preset & initData, dsp_preset_edit_callback & callback) : m_initData(initData), m_callback(callback) {}
|
||||
|
||||
enum { IDD = IDD_DSP };
|
||||
|
||||
enum {
|
||||
RangeMin = -20,
|
||||
RangeMax = 20,
|
||||
|
||||
RangeTotal = RangeMax - RangeMin
|
||||
};
|
||||
|
||||
BEGIN_MSG_MAP_EX(CMyDSPPopup)
|
||||
MSG_WM_INITDIALOG(OnInitDialog)
|
||||
COMMAND_HANDLER_EX(IDOK, BN_CLICKED, OnButton)
|
||||
COMMAND_HANDLER_EX(IDCANCEL, BN_CLICKED, OnButton)
|
||||
MSG_WM_HSCROLL(OnHScroll)
|
||||
END_MSG_MAP()
|
||||
|
||||
private:
|
||||
|
||||
BOOL OnInitDialog(CWindow, LPARAM) {
|
||||
m_slider = GetDlgItem(IDC_SLIDER);
|
||||
m_slider.SetRange(0, RangeTotal);
|
||||
|
||||
{
|
||||
float val;
|
||||
dsp_sample::parse_preset(val, m_initData);
|
||||
m_slider.SetPos( pfc::clip_t<t_int32>( pfc::rint32(val), RangeMin, RangeMax ) - RangeMin );
|
||||
RefreshLabel(val);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void OnButton(UINT, int id, CWindow) {
|
||||
EndDialog(id);
|
||||
}
|
||||
|
||||
void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar pScrollBar) {
|
||||
float val;
|
||||
val = (float) ( m_slider.GetPos() + RangeMin );
|
||||
|
||||
{
|
||||
dsp_preset_impl preset;
|
||||
dsp_sample::make_preset(val, preset);
|
||||
m_callback.on_preset_changed(preset);
|
||||
}
|
||||
RefreshLabel(val);
|
||||
}
|
||||
|
||||
void RefreshLabel(float val) {
|
||||
pfc::string_formatter msg; msg << pfc::format_float(val) << " dB";
|
||||
::uSetDlgItemText(*this, IDC_SLIDER_LABEL, msg);
|
||||
}
|
||||
|
||||
const dsp_preset & m_initData; // modal dialog so we can reference this caller-owned object.
|
||||
dsp_preset_edit_callback & m_callback;
|
||||
|
||||
CTrackBarCtrl m_slider;
|
||||
};
|
||||
|
||||
static void RunDSPConfigPopup(const dsp_preset & p_data,HWND p_parent,dsp_preset_edit_callback & p_callback) {
|
||||
CMyDSPPopup popup(p_data, p_callback);
|
||||
if (popup.DoModal(p_parent) != IDOK) {
|
||||
// If the dialog exited with something else than IDOK,k
|
||||
// tell host that the editing has been cancelled by sending it old preset data that we got initialized with
|
||||
p_callback.on_preset_changed(p_data);
|
||||
}
|
||||
}
|
||||
227
foobar2000/foo_sample/foo_sample.rc
Normal file
227
foobar2000/foo_sample/foo_sample.rc
Normal file
@@ -0,0 +1,227 @@
|
||||
// Microsoft Visual C++ generated resource script.
|
||||
//
|
||||
#include "resource.h"
|
||||
|
||||
#define APSTUDIO_READONLY_SYMBOLS
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 2 resource.
|
||||
//
|
||||
#include "afxres.h"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#undef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// English (United States) resources
|
||||
|
||||
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
||||
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
#pragma code_page(1252)
|
||||
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TEXTINCLUDE
|
||||
//
|
||||
|
||||
1 TEXTINCLUDE
|
||||
BEGIN
|
||||
"resource.h\0"
|
||||
END
|
||||
|
||||
2 TEXTINCLUDE
|
||||
BEGIN
|
||||
"#include ""afxres.h""\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
3 TEXTINCLUDE
|
||||
BEGIN
|
||||
"\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
#endif // APSTUDIO_INVOKED
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Dialog
|
||||
//
|
||||
|
||||
IDD_MYPREFERENCES DIALOGEX 0, 0, 332, 288
|
||||
STYLE DS_SETFONT | WS_CHILD
|
||||
FONT 8, "Microsoft Sans Serif", 400, 0, 0x0
|
||||
BEGIN
|
||||
RTEXT "Bogo setting 1:",IDC_STATIC,44,60,59,8
|
||||
EDITTEXT IDC_BOGO1,104,58,40,12,ES_AUTOHSCROLL | ES_NUMBER
|
||||
RTEXT "Bogo setting 2:",IDC_STATIC,44,82,59,8
|
||||
EDITTEXT IDC_BOGO2,104,80,40,12,ES_AUTOHSCROLL | ES_NUMBER
|
||||
LTEXT "This is a sample preferences page with meaningless settings.",IDC_STATIC,52,24,204,8
|
||||
END
|
||||
|
||||
IDD_PLAYBACK_STATE DIALOGEX 0, 0, 260, 95
|
||||
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
|
||||
CAPTION "Playback State Demo"
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
LTEXT "Pattern:",IDC_STATIC,12,8,28,8
|
||||
EDITTEXT IDC_PATTERN,8,16,244,12,ES_AUTOHSCROLL
|
||||
LTEXT "State:",IDC_STATIC,12,36,21,8
|
||||
EDITTEXT IDC_STATE,8,44,244,12,ES_AUTOHSCROLL | ES_READONLY
|
||||
PUSHBUTTON "Context menu test area - right click me",IDC_CONTEXTMENU,8,60,244,14
|
||||
PUSHBUTTON "Play",IDC_PLAY,12,76,36,14
|
||||
PUSHBUTTON "Pause",IDC_PAUSE,52,76,36,14
|
||||
PUSHBUTTON "Stop",IDC_STOP,92,76,36,14
|
||||
PUSHBUTTON "Prev",IDC_PREV,132,76,36,14
|
||||
PUSHBUTTON "Next",IDC_NEXT,172,76,36,14
|
||||
PUSHBUTTON "Rand",IDC_RAND,212,76,36,14
|
||||
END
|
||||
|
||||
IDD_DSP DIALOGEX 0, 0, 284, 83
|
||||
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
|
||||
CAPTION "Sample DSP Configuration"
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
DEFPUSHBUTTON "OK",IDOK,172,64,50,14
|
||||
PUSHBUTTON "Cancel",IDCANCEL,226,64,50,14
|
||||
CONTROL "",IDC_SLIDER,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,12,12,260,24
|
||||
CTEXT "N dB",IDC_SLIDER_LABEL,120,44,44,8
|
||||
END
|
||||
|
||||
IDD_UI_ELEMENT DIALOGEX 0, 0, 221, 113
|
||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
CONTROL "Lock min width @ 200 units",IDC_LOCK_MIN_WIDTH,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,8,20,104,10
|
||||
CONTROL "Lock min height @ 200 units",IDC_LOCK_MIN_HEIGHT,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,8,32,107,10
|
||||
LTEXT "Current size: XXX x XXX units / XXX x XXX pixels",IDC_STATIC_SIZE,8,4,168,8
|
||||
CONTROL "Lock max width @ 400 units",IDC_LOCK_MAX_WIDTH,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,8,48,106,10
|
||||
CONTROL "Lock max height @ 400 units",IDC_LOCK_MAX_HEIGHT,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,8,60,108,10
|
||||
END
|
||||
|
||||
IDD_THREADS DIALOGEX 0, 0, 364, 232
|
||||
STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
|
||||
CAPTION "UI and Threads Demo"
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
DEFPUSHBUTTON "Start",IDOK,252,200,50,14
|
||||
PUSHBUTTON "Cancel",IDCANCEL,306,200,50,14
|
||||
LISTBOX IDC_LIST,4,20,356,176,LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP
|
||||
LTEXT "Header",IDC_HEADER,8,4,348,8
|
||||
END
|
||||
|
||||
IDD_LISTCONTROL_DEMO DIALOGEX 0, 0, 311, 177
|
||||
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
|
||||
CAPTION "Dialog"
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
CONTROL "",IDC_LIST1,"SysListView32",LVS_ALIGNLEFT | WS_BORDER | WS_TABSTOP,12,12,288,156
|
||||
END
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DESIGNINFO
|
||||
//
|
||||
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
GUIDELINES DESIGNINFO
|
||||
BEGIN
|
||||
IDD_MYPREFERENCES, DIALOG
|
||||
BEGIN
|
||||
LEFTMARGIN, 7
|
||||
RIGHTMARGIN, 325
|
||||
TOPMARGIN, 7
|
||||
BOTTOMMARGIN, 281
|
||||
END
|
||||
|
||||
IDD_PLAYBACK_STATE, DIALOG
|
||||
BEGIN
|
||||
LEFTMARGIN, 7
|
||||
RIGHTMARGIN, 253
|
||||
TOPMARGIN, 7
|
||||
BOTTOMMARGIN, 88
|
||||
END
|
||||
|
||||
IDD_DSP, DIALOG
|
||||
BEGIN
|
||||
LEFTMARGIN, 7
|
||||
RIGHTMARGIN, 277
|
||||
TOPMARGIN, 7
|
||||
BOTTOMMARGIN, 76
|
||||
END
|
||||
|
||||
IDD_UI_ELEMENT, DIALOG
|
||||
BEGIN
|
||||
LEFTMARGIN, 7
|
||||
RIGHTMARGIN, 214
|
||||
TOPMARGIN, 7
|
||||
BOTTOMMARGIN, 106
|
||||
END
|
||||
|
||||
IDD_THREADS, DIALOG
|
||||
BEGIN
|
||||
LEFTMARGIN, 7
|
||||
RIGHTMARGIN, 357
|
||||
TOPMARGIN, 7
|
||||
BOTTOMMARGIN, 225
|
||||
END
|
||||
|
||||
IDD_LISTCONTROL_DEMO, DIALOG
|
||||
BEGIN
|
||||
LEFTMARGIN, 7
|
||||
RIGHTMARGIN, 304
|
||||
TOPMARGIN, 7
|
||||
BOTTOMMARGIN, 170
|
||||
END
|
||||
END
|
||||
#endif // APSTUDIO_INVOKED
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// AFX_DIALOG_LAYOUT
|
||||
//
|
||||
|
||||
IDD_UI_ELEMENT AFX_DIALOG_LAYOUT
|
||||
BEGIN
|
||||
0
|
||||
END
|
||||
|
||||
IDD_THREADS AFX_DIALOG_LAYOUT
|
||||
BEGIN
|
||||
0
|
||||
END
|
||||
|
||||
IDD_LISTCONTROL_DEMO AFX_DIALOG_LAYOUT
|
||||
BEGIN
|
||||
0
|
||||
END
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Icon
|
||||
//
|
||||
|
||||
// Icon with lowest ID value placed first to ensure application icon
|
||||
// remains consistent on all systems.
|
||||
IDI_SCROLL ICON "..\\..\\libPPUI\\IDI_SCROLL.ico"
|
||||
|
||||
#endif // English (United States) resources
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
#ifndef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 3 resource.
|
||||
//
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#endif // not APSTUDIO_INVOKED
|
||||
|
||||
55
foobar2000/foo_sample/foo_sample.sln
Normal file
55
foobar2000/foo_sample/foo_sample.sln
Normal file
@@ -0,0 +1,55 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27130.2027
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "foo_sample", "foo_sample.vcxproj", "{85FBFD09-0099-4FE9-9DB6-78DB6F60F817}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "foobar2000_SDK", "..\SDK\foobar2000_SDK.vcxproj", "{E8091321-D79D-4575-86EF-064EA1A4A20D}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "pfc", "..\..\pfc\pfc.vcxproj", "{EBFFFB4E-261D-44D3-B89C-957B31A0BF9C}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "foobar2000_sdk_helpers", "..\helpers\foobar2000_sdk_helpers.vcxproj", "{EE47764E-A202-4F85-A767-ABDAB4AFF35F}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "foobar2000_component_client", "..\foobar2000_component_client\foobar2000_component_client.vcxproj", "{71AD2674-065B-48F5-B8B0-E1F9D3892081}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libPPUI", "..\..\libPPUI\libPPUI.vcxproj", "{7729EB82-4069-4414-964B-AD399091A03F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|x86 = Debug|x86
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{85FBFD09-0099-4FE9-9DB6-78DB6F60F817}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{85FBFD09-0099-4FE9-9DB6-78DB6F60F817}.Debug|x86.Build.0 = Debug|Win32
|
||||
{85FBFD09-0099-4FE9-9DB6-78DB6F60F817}.Release|x86.ActiveCfg = Release|Win32
|
||||
{85FBFD09-0099-4FE9-9DB6-78DB6F60F817}.Release|x86.Build.0 = Release|Win32
|
||||
{E8091321-D79D-4575-86EF-064EA1A4A20D}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{E8091321-D79D-4575-86EF-064EA1A4A20D}.Debug|x86.Build.0 = Debug|Win32
|
||||
{E8091321-D79D-4575-86EF-064EA1A4A20D}.Release|x86.ActiveCfg = Release|Win32
|
||||
{E8091321-D79D-4575-86EF-064EA1A4A20D}.Release|x86.Build.0 = Release|Win32
|
||||
{EBFFFB4E-261D-44D3-B89C-957B31A0BF9C}.Debug|x86.ActiveCfg = Debug FB2K|Win32
|
||||
{EBFFFB4E-261D-44D3-B89C-957B31A0BF9C}.Debug|x86.Build.0 = Debug FB2K|Win32
|
||||
{EBFFFB4E-261D-44D3-B89C-957B31A0BF9C}.Release|x86.ActiveCfg = Release FB2K|Win32
|
||||
{EBFFFB4E-261D-44D3-B89C-957B31A0BF9C}.Release|x86.Build.0 = Release FB2K|Win32
|
||||
{EE47764E-A202-4F85-A767-ABDAB4AFF35F}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{EE47764E-A202-4F85-A767-ABDAB4AFF35F}.Debug|x86.Build.0 = Debug|Win32
|
||||
{EE47764E-A202-4F85-A767-ABDAB4AFF35F}.Release|x86.ActiveCfg = Release|Win32
|
||||
{EE47764E-A202-4F85-A767-ABDAB4AFF35F}.Release|x86.Build.0 = Release|Win32
|
||||
{71AD2674-065B-48F5-B8B0-E1F9D3892081}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{71AD2674-065B-48F5-B8B0-E1F9D3892081}.Debug|x86.Build.0 = Debug|Win32
|
||||
{71AD2674-065B-48F5-B8B0-E1F9D3892081}.Release|x86.ActiveCfg = Release|Win32
|
||||
{71AD2674-065B-48F5-B8B0-E1F9D3892081}.Release|x86.Build.0 = Release|Win32
|
||||
{7729EB82-4069-4414-964B-AD399091A03F}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{7729EB82-4069-4414-964B-AD399091A03F}.Debug|x86.Build.0 = Debug|Win32
|
||||
{7729EB82-4069-4414-964B-AD399091A03F}.Release|x86.ActiveCfg = Release|Win32
|
||||
{7729EB82-4069-4414-964B-AD399091A03F}.Release|x86.Build.0 = Release|Win32
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C30C2B27-E29B-4792-B392-ED729AF2CF49}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
159
foobar2000/foo_sample/foo_sample.vcxproj
Normal file
159
foobar2000/foo_sample/foo_sample.vcxproj
Normal file
@@ -0,0 +1,159 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|Win32">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|Win32">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{85FBFD09-0099-4FE9-9DB6-78DB6F60F817}</ProjectGuid>
|
||||
<RootNamespace>foo_input_raw</RootNamespace>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<PlatformToolset>v141</PlatformToolset>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<PlatformToolset>v141</PlatformToolset>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<_ProjectFileVersion>10.0.30319.1</_ProjectFileVersion>
|
||||
<OutDir Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(SolutionDir)$(Configuration)\</OutDir>
|
||||
<IntDir Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(Configuration)\</IntDir>
|
||||
<LinkIncremental Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</LinkIncremental>
|
||||
<OutDir Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(SolutionDir)$(Configuration)\</OutDir>
|
||||
<IntDir Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(Configuration)\</IntDir>
|
||||
<LinkIncremental Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">false</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<ClCompile>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>stdafx.h</PrecompiledHeaderFile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
<TreatSpecificWarningsAsErrors>4715</TreatSpecificWarningsAsErrors>
|
||||
<AdditionalIncludeDirectories>..;../..</AdditionalIncludeDirectories>
|
||||
<FloatingPointModel>Fast</FloatingPointModel>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<RandomizedBaseAddress>false</RandomizedBaseAddress>
|
||||
<DataExecutionPrevention>
|
||||
</DataExecutionPrevention>
|
||||
<TargetMachine>MachineX86</TargetMachine>
|
||||
<AdditionalDependencies>../shared/shared.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>stdafx.h</PrecompiledHeaderFile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
<AdditionalOptions>/d2notypeopt %(AdditionalOptions)</AdditionalOptions>
|
||||
<TreatSpecificWarningsAsErrors>4715</TreatSpecificWarningsAsErrors>
|
||||
<BufferSecurityCheck>false</BufferSecurityCheck>
|
||||
<StringPooling>true</StringPooling>
|
||||
<OmitFramePointers>true</OmitFramePointers>
|
||||
<AdditionalIncludeDirectories>..;../..</AdditionalIncludeDirectories>
|
||||
<FloatingPointModel>Fast</FloatingPointModel>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<RandomizedBaseAddress>false</RandomizedBaseAddress>
|
||||
<DataExecutionPrevention>
|
||||
</DataExecutionPrevention>
|
||||
<TargetMachine>MachineX86</TargetMachine>
|
||||
<AdditionalDependencies>../shared/shared.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="contextmenu.cpp" />
|
||||
<ClCompile Include="decode.cpp" />
|
||||
<ClCompile Include="dsp.cpp" />
|
||||
<ClCompile Include="initquit.cpp" />
|
||||
<ClCompile Include="input_raw.cpp" />
|
||||
<ClCompile Include="listcontrol-advanced.cpp" />
|
||||
<ClCompile Include="listcontrol-ownerdata.cpp" />
|
||||
<ClCompile Include="listcontrol-simple.cpp" />
|
||||
<ClCompile Include="main.cpp" />
|
||||
<ClCompile Include="mainmenu-dynamic.cpp" />
|
||||
<ClCompile Include="mainmenu.cpp" />
|
||||
<ClCompile Include="IO.cpp" />
|
||||
<ClCompile Include="PCH.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="playback_state.cpp" />
|
||||
<ClCompile Include="playback_stream_capture.cpp" />
|
||||
<ClCompile Include="preferences.cpp" />
|
||||
<ClCompile Include="rating.cpp" />
|
||||
<ClCompile Include="ui_and_threads.cpp" />
|
||||
<ClCompile Include="ui_element.cpp" />
|
||||
<ClCompile Include="ui_element_dialog.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="playback_stream_capture.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="stdafx.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="foo_sample.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="readme.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\libPPUI\libPPUI.vcxproj">
|
||||
<Project>{7729eb82-4069-4414-964b-ad399091a03f}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\pfc\pfc.vcxproj">
|
||||
<Project>{ebfffb4e-261d-44d3-b89c-957b31a0bf9c}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\foobar2000_component_client\foobar2000_component_client.vcxproj">
|
||||
<Project>{71ad2674-065b-48f5-b8b0-e1f9d3892081}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\helpers\foobar2000_sdk_helpers.vcxproj">
|
||||
<Project>{ee47764e-a202-4f85-a767-abdab4aff35f}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SDK\foobar2000_SDK.vcxproj">
|
||||
<Project>{e8091321-d79d-4575-86ef-064ea1a4a20d}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="..\..\libPPUI\IDI_SCROLL.ico" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
103
foobar2000/foo_sample/foo_sample.vcxproj.filters
Normal file
103
foobar2000/foo_sample/foo_sample.vcxproj.filters
Normal file
@@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hpp;hxx;hm;inl;inc;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="contextmenu.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="decode.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="dsp.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="initquit.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="input_raw.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="main.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="mainmenu.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="PCH.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="playback_state.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="preferences.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ui_element.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ui_element_dialog.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="rating.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="IO.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ui_and_threads.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="listcontrol-advanced.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="listcontrol-simple.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="listcontrol-ownerdata.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="mainmenu-dynamic.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="playback_stream_capture.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="resource.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="stdafx.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="playback_stream_capture.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="foo_sample.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="readme.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="..\..\libPPUI\IDI_SCROLL.ico">
|
||||
<Filter>Resource Files</Filter>
|
||||
</Image>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
15
foobar2000/foo_sample/initquit.cpp
Normal file
15
foobar2000/foo_sample/initquit.cpp
Normal file
@@ -0,0 +1,15 @@
|
||||
#include "stdafx.h"
|
||||
|
||||
// Sample initquit implementation. See also: initquit class documentation in relevant header.
|
||||
|
||||
class myinitquit : public initquit {
|
||||
public:
|
||||
void on_init() {
|
||||
console::print("Sample component: on_init()");
|
||||
}
|
||||
void on_quit() {
|
||||
console::print("Sample component: on_quit()");
|
||||
}
|
||||
};
|
||||
|
||||
static initquit_factory_t<myinitquit> g_myinitquit_factory;
|
||||
103
foobar2000/foo_sample/input_raw.cpp
Normal file
103
foobar2000/foo_sample/input_raw.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
#include "stdafx.h"
|
||||
|
||||
enum {
|
||||
raw_bits_per_sample = 16,
|
||||
raw_channels = 2,
|
||||
raw_sample_rate = 44100,
|
||||
|
||||
raw_bytes_per_sample = raw_bits_per_sample / 8,
|
||||
raw_total_sample_width = raw_bytes_per_sample * raw_channels,
|
||||
};
|
||||
|
||||
// Note that input class does *not* implement virtual methods or derive from interface classes.
|
||||
// Our methods get called over input framework templates. See input_singletrack_impl for descriptions of what each method does.
|
||||
// input_stubs just provides stub implementations of mundane methods that are irrelevant for most implementations.
|
||||
class input_raw : public input_stubs {
|
||||
public:
|
||||
void open(service_ptr_t<file> p_filehint,const char * p_path,t_input_open_reason p_reason,abort_callback & p_abort) {
|
||||
if (p_reason == input_open_info_write) throw exception_io_unsupported_format();//our input does not support retagging.
|
||||
m_file = p_filehint;//p_filehint may be null, hence next line
|
||||
input_open_file_helper(m_file,p_path,p_reason,p_abort);//if m_file is null, opens file with appropriate privileges for our operation (read/write for writing tags, read-only otherwise).
|
||||
}
|
||||
|
||||
void get_info(file_info & p_info,abort_callback & p_abort) {
|
||||
t_filesize size = m_file->get_size(p_abort);
|
||||
//note that the file size is not always known, for an example, live streams and alike have no defined size and filesize_invalid is returned
|
||||
if (size != filesize_invalid) {
|
||||
//file size is known, let's set length
|
||||
p_info.set_length(audio_math::samples_to_time( size / raw_total_sample_width, raw_sample_rate));
|
||||
}
|
||||
//note that the values below should be based on contents of the file itself, NOT on user-configurable variables for an example. To report info that changes independently from file contents, use get_dynamic_info/get_dynamic_info_track instead.
|
||||
p_info.info_set_int("samplerate",raw_sample_rate);
|
||||
p_info.info_set_int("channels",raw_channels);
|
||||
p_info.info_set_int("bitspersample",raw_bits_per_sample);
|
||||
|
||||
// Indicate whether this is a fixedpoint or floatingpoint stream, when using bps >= 32
|
||||
// As 32bit fixedpoint can't be decoded losslessly by fb2k, does not fit in float32 audio_sample.
|
||||
if ( raw_bits_per_sample >= 32 ) p_info.info_set("bitspersample_extra", "fixed-point");
|
||||
|
||||
p_info.info_set("encoding","lossless");
|
||||
p_info.info_set_bitrate((raw_bits_per_sample * raw_channels * raw_sample_rate + 500 /* rounding for bps to kbps*/ ) / 1000 /* bps to kbps */);
|
||||
|
||||
}
|
||||
t_filestats get_file_stats(abort_callback & p_abort) {return m_file->get_stats(p_abort);}
|
||||
|
||||
void decode_initialize(unsigned p_flags,abort_callback & p_abort) {
|
||||
m_file->reopen(p_abort);//equivalent to seek to zero, except it also works on nonseekable streams
|
||||
}
|
||||
bool decode_run(audio_chunk & p_chunk,abort_callback & p_abort) {
|
||||
enum {
|
||||
deltaread = 1024,
|
||||
};
|
||||
|
||||
const size_t deltaReadBytes = deltaread * raw_total_sample_width;
|
||||
// Prepare buffer
|
||||
m_buffer.set_size(deltaReadBytes);
|
||||
// Read bytes
|
||||
size_t got = m_file->read(m_buffer.get_ptr(), deltaReadBytes,p_abort) / raw_total_sample_width;
|
||||
|
||||
// EOF?
|
||||
if (got == 0) return false;
|
||||
|
||||
// This converts the data that we've read to the audio_chunk's internal format, audio_sample (float 32-bit).
|
||||
// audio_sample is the audio data format that all fb2k code works with.
|
||||
p_chunk.set_data_fixedpoint(m_buffer.get_ptr(), got * raw_total_sample_width,raw_sample_rate,raw_channels,raw_bits_per_sample,audio_chunk::g_guess_channel_config(raw_channels));
|
||||
|
||||
//processed successfully, no EOF
|
||||
return true;
|
||||
}
|
||||
void decode_seek(double p_seconds,abort_callback & p_abort) {
|
||||
m_file->ensure_seekable();//throw exceptions if someone called decode_seek() despite of our input having reported itself as nonseekable.
|
||||
// IMPORTANT: convert time to sample offset with proper rounding! audio_math::time_to_samples does this properly for you.
|
||||
t_filesize target = audio_math::time_to_samples(p_seconds,raw_sample_rate) * raw_total_sample_width;
|
||||
|
||||
// get_size_ex fails (throws exceptions) if size is not known (where get_size would return filesize_invalid). Should never fail on seekable streams (if it does it's not our problem anymore).
|
||||
t_filesize max = m_file->get_size_ex(p_abort);
|
||||
if (target > max) target = max;//clip seek-past-eof attempts to legal range (next decode_run() call will just signal EOF).
|
||||
|
||||
m_file->seek(target,p_abort);
|
||||
}
|
||||
bool decode_can_seek() {return m_file->can_seek();}
|
||||
bool decode_get_dynamic_info(file_info & p_out, double & p_timestamp_delta) {return false;} // deals with dynamic information such as VBR bitrates
|
||||
bool decode_get_dynamic_info_track(file_info & p_out, double & p_timestamp_delta) {return false;} // deals with dynamic information such as track changes in live streams
|
||||
void decode_on_idle(abort_callback & p_abort) {m_file->on_idle(p_abort);}
|
||||
|
||||
void retag(const file_info & p_info,abort_callback & p_abort) {throw exception_io_unsupported_format();}
|
||||
|
||||
static bool g_is_our_content_type(const char * p_content_type) {return false;} // match against supported mime types here
|
||||
static bool g_is_our_path(const char * p_path,const char * p_extension) {return stricmp_utf8(p_extension,"raw") == 0;}
|
||||
static const char * g_get_name() { return "foo_sample raw input"; }
|
||||
static const GUID g_get_guid() {
|
||||
// GUID of the decoder. Replace with your own when reusing code.
|
||||
static const GUID I_am_foo_sample_and_this_is_my_decoder_GUID = { 0xd9c01c8d, 0x69c5, 0x4eec,{ 0xa2, 0x1c, 0x1d, 0x14, 0xef, 0x65, 0xbf, 0x8b } };
|
||||
return I_am_foo_sample_and_this_is_my_decoder_GUID;
|
||||
}
|
||||
public:
|
||||
service_ptr_t<file> m_file;
|
||||
pfc::array_t<t_uint8> m_buffer;
|
||||
};
|
||||
|
||||
static input_singletrack_factory_t<input_raw> g_input_raw_factory;
|
||||
|
||||
// Declare .RAW as a supported file type to make it show in "open file" dialog etc.
|
||||
DECLARE_FILE_TYPE("Raw files","*.RAW");
|
||||
323
foobar2000/foo_sample/listcontrol-advanced.cpp
Normal file
323
foobar2000/foo_sample/listcontrol-advanced.cpp
Normal file
@@ -0,0 +1,323 @@
|
||||
// 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>
|
||||
|
||||
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 );
|
||||
|
||||
ShowWindow(SW_SHOW);
|
||||
|
||||
return TRUE; // system should set focus
|
||||
}
|
||||
|
||||
CListControlDemo m_list;
|
||||
};
|
||||
}
|
||||
|
||||
// Called from mainmenu.cpp
|
||||
void RunListControlAdvancedDemo() {
|
||||
// automatically creates the dialog with object lifetime management and modeless dialog registration
|
||||
fb2k::newDialog<CListControlAdvancedDemoDialog>();
|
||||
}
|
||||
|
||||
204
foobar2000/foo_sample/listcontrol-ownerdata.cpp
Normal file
204
foobar2000/foo_sample/listcontrol-ownerdata.cpp
Normal file
@@ -0,0 +1,204 @@
|
||||
#include "stdafx.h"
|
||||
|
||||
// Owner-data CListControl use demo
|
||||
// CListControlOwnerData with callbacks
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "resource.h"
|
||||
#include <helpers/atl-misc.h>
|
||||
#include <libPPUI/CListControlOwnerData.h>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
struct listData_t {
|
||||
std::string m_key, m_value;
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
class CListControlOwnerDataDemoDialog : public CDialogImpl<CListControlOwnerDataDemoDialog>, private IListControlOwnerDataSource {
|
||||
public:
|
||||
|
||||
// CListControlOwnerData constructor requires ptr to IListControlOwnerDataSource object
|
||||
CListControlOwnerDataDemoDialog() : m_list(this) {}
|
||||
|
||||
enum { IDD = IDD_LISTCONTROL_DEMO };
|
||||
|
||||
BEGIN_MSG_MAP_EX(CListControlOwnerDataDemoDialog)
|
||||
MSG_WM_INITDIALOG(OnInitDialog)
|
||||
COMMAND_HANDLER_EX(IDCANCEL, BN_CLICKED, OnCancel)
|
||||
MSG_WM_CONTEXTMENU(OnContextMenu)
|
||||
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 );
|
||||
|
||||
// never hardcode values in pixels, always use screen DPI
|
||||
auto DPI = m_list.GetDPI();
|
||||
m_list.AddColumn( "Name", MulDiv(100, DPI.cx, 96 ) );
|
||||
m_list.AddColumn( "Value", MulDiv(150, DPI.cx, 96 ) );
|
||||
|
||||
ShowWindow(SW_SHOW);
|
||||
|
||||
return TRUE; // system should set focus
|
||||
}
|
||||
|
||||
// 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 = m_list.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 << m_list.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 < m_list.GetItemCount(); ++ walk) {
|
||||
if ( m_list.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:
|
||||
m_list.SelectAll(); // trivial
|
||||
break;
|
||||
case ID_SELECTNONE:
|
||||
m_list.SelectNone(); // trivial
|
||||
break;
|
||||
case ID_INVERTSEL:
|
||||
{
|
||||
auto mask = m_list.GetSelectionMask();
|
||||
m_list.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;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// IListControlOwnerDataSource methods
|
||||
|
||||
size_t listGetItemCount( ctx_t ctx ) override {
|
||||
PFC_ASSERT( ctx == &m_list ); // ctx is a pointer to the object calling us
|
||||
return m_data.size();
|
||||
}
|
||||
pfc::string8 listGetSubItemText( ctx_t, size_t item, size_t subItem ) override {
|
||||
auto & rec = m_data[item];
|
||||
switch(subItem) {
|
||||
case 0:
|
||||
return rec.m_key.c_str();
|
||||
case 1:
|
||||
return rec.m_value.c_str();
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
||||
bool listCanReorderItems( ctx_t ) override {
|
||||
return true;
|
||||
}
|
||||
bool listReorderItems( ctx_t, const size_t* order, size_t count) override {
|
||||
PFC_ASSERT( count == m_data.size() );
|
||||
pfc::reorder_t( m_data, order, count );
|
||||
return true;
|
||||
}
|
||||
bool listRemoveItems( ctx_t, pfc::bit_array const & mask) override {
|
||||
pfc::remove_mask_t( m_data, mask );
|
||||
return true;
|
||||
}
|
||||
void listItemAction(ctx_t, size_t item) override {
|
||||
m_list.TableEdit_Start( item, 1 );
|
||||
}
|
||||
void listSubItemClicked( ctx_t, size_t item, size_t subItem) override {
|
||||
if ( subItem == 1 ) {
|
||||
m_list.TableEdit_Start( item, subItem );
|
||||
}
|
||||
}
|
||||
void listSetEditField(ctx_t ctx, size_t item, size_t subItem, const char * val) override {
|
||||
if ( subItem == 1 ) {
|
||||
m_data[item].m_value = val;
|
||||
}
|
||||
}
|
||||
bool listIsColumnEditable( ctx_t, size_t subItem ) override {
|
||||
return subItem == 1;
|
||||
}
|
||||
|
||||
|
||||
std::vector< listData_t > m_data = makeListData();
|
||||
|
||||
CListControlOwnerData m_list;
|
||||
};
|
||||
}
|
||||
// Called from mainmenu.cpp
|
||||
void RunListControlOwnerDataDemo() {
|
||||
// automatically creates the dialog with object lifetime management and modeless dialog registration
|
||||
fb2k::newDialog<CListControlOwnerDataDemoDialog>();
|
||||
}
|
||||
163
foobar2000/foo_sample/listcontrol-simple.cpp
Normal file
163
foobar2000/foo_sample/listcontrol-simple.cpp
Normal file
@@ -0,0 +1,163 @@
|
||||
#include "stdafx.h"
|
||||
|
||||
// Simple CListControl use demo
|
||||
// CListControlSimple
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "resource.h"
|
||||
#include <helpers/atl-misc.h>
|
||||
#include <libPPUI/CListControlSimple.h>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
struct listData_t {
|
||||
std::string m_key, m_value;
|
||||
};
|
||||
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 = "sample value";
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
class CListControlSimpleDemoDialog : public CDialogImpl<CListControlSimpleDemoDialog> {
|
||||
public:
|
||||
|
||||
enum { IDD = IDD_LISTCONTROL_DEMO };
|
||||
|
||||
BEGIN_MSG_MAP_EX(CListControlOwnerDataDemoDialog)
|
||||
MSG_WM_INITDIALOG(OnInitDialog)
|
||||
COMMAND_HANDLER_EX(IDCANCEL, BN_CLICKED, OnCancel)
|
||||
MSG_WM_CONTEXTMENU(OnContextMenu)
|
||||
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 );
|
||||
|
||||
// never hardcode values in pixels, always use screen DPI
|
||||
auto DPI = m_list.GetDPI();
|
||||
m_list.AddColumn( "Name", MulDiv(100, DPI.cx, 96 ) );
|
||||
m_list.AddColumn( "Value", MulDiv(150, DPI.cx, 96 ) );
|
||||
|
||||
{
|
||||
auto data = makeListData();
|
||||
|
||||
m_list.SetItemCount( data.size( ) );
|
||||
for( size_t walk = 0; walk < data.size(); ++ walk ) {
|
||||
auto & rec = data[walk];
|
||||
m_list.SetItemText( walk, 0, rec.m_key.c_str() );
|
||||
m_list.SetItemText( walk, 1, rec.m_value.c_str() );
|
||||
}
|
||||
}
|
||||
|
||||
ShowWindow(SW_SHOW);
|
||||
|
||||
return TRUE; // system should set focus
|
||||
}
|
||||
|
||||
// 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 = m_list.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 << m_list.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 < m_list.GetItemCount(); ++ walk) {
|
||||
if ( m_list.IsItemSelected( walk ) ) {
|
||||
msg << "#" << (walk+1) << "\r\n";
|
||||
}
|
||||
}
|
||||
msg << "End selected items.";
|
||||
// popup_message : non-blocking MessageBox equivalent
|
||||
popup_message::g_show(msg, "Information");
|
||||
}
|
||||
break;
|
||||
case ID_SELECTALL:
|
||||
m_list.SelectAll(); // trivial
|
||||
break;
|
||||
case ID_SELECTNONE:
|
||||
m_list.SelectNone(); // trivial
|
||||
break;
|
||||
case ID_INVERTSEL:
|
||||
{
|
||||
auto mask = m_list.GetSelectionMask();
|
||||
m_list.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;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
CListControlSimple m_list;
|
||||
};
|
||||
}
|
||||
|
||||
// Called from mainmenu.cpp
|
||||
void RunListControlSimpleDemo() {
|
||||
fb2k::newDialog<CListControlSimpleDemoDialog>();
|
||||
}
|
||||
11
foobar2000/foo_sample/main.cpp
Normal file
11
foobar2000/foo_sample/main.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#include "stdafx.h"
|
||||
|
||||
// Declaration of your component's version information
|
||||
// Since foobar2000 v1.0 having at least one of these in your DLL is mandatory to let the troubleshooter tell different versions of your component apart.
|
||||
// Note that it is possible to declare multiple components within one DLL, but it's strongly recommended to keep only one declaration per DLL.
|
||||
// As for 1.1, the version numbers are used by the component update finder to find updates; for that to work, you must have ONLY ONE declaration per DLL. If there are multiple declarations, the component is assumed to be outdated and a version number of "0" is assumed, to overwrite the component with whatever is currently on the site assuming that it comes with proper version numbers.
|
||||
DECLARE_COMPONENT_VERSION("Sample Component","1.0","about message goes here");
|
||||
|
||||
|
||||
// This will prevent users from renaming your component around (important for proper troubleshooter behaviors) or loading multiple instances of it.
|
||||
VALIDATE_COMPONENT_FILENAME("foo_sample.dll");
|
||||
120
foobar2000/foo_sample/mainmenu-dynamic.cpp
Normal file
120
foobar2000/foo_sample/mainmenu-dynamic.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "stdafx.h"
|
||||
#include <vector>
|
||||
|
||||
namespace { // anon namespace everything, it's not accessible by means other than the service factory
|
||||
|
||||
// The command ID.
|
||||
// Generate a new GUID when reusing code.
|
||||
static const GUID guid_menucommand = { 0xab754b0b, 0x204, 0x4471, { 0xb5, 0x29, 0xff, 0x73, 0xae, 0x51, 0x5d, 0xe9 } };
|
||||
|
||||
// Shared with mainmenu.cpp
|
||||
static const GUID guid_mainmenu_group_id = { 0x44963e7a, 0x4b2a, 0x4588, { 0xb0, 0x17, 0xa8, 0x69, 0x18, 0xcb, 0x8a, 0xa5 } };
|
||||
|
||||
class sample_command : public mainmenu_node_command {
|
||||
public:
|
||||
sample_command( size_t index ) : m_index(index) {
|
||||
}
|
||||
void get_display(pfc::string_base & text, t_uint32 & flags) override {
|
||||
flags = 0;
|
||||
text = PFC_string_formatter() << "Test dynamic item #" << m_index;
|
||||
}
|
||||
void execute(service_ptr_t<service_base> callback) override {
|
||||
popup_message::g_show(PFC_string_formatter() << "Invoked test menu item #" << m_index, "Information");
|
||||
}
|
||||
GUID get_guid() override {
|
||||
// This method returns our subcommand ID.
|
||||
// Dynamic commands are identified by a pair of GUIDs:
|
||||
// - command ID ( see: mainmenu_commands interface )
|
||||
// - subcommand ID ( identifier of one of the dynamic items )
|
||||
// Subcommand identifiers don't have to be actually globally unique,
|
||||
// as long as they're unique among the subcommand identifiers for this command ID.
|
||||
|
||||
// In our case, we'll just create a makeshift GUID from a hash of the index.
|
||||
// This is perfectly okay for production code - as long as your command ID is a proper GUID!
|
||||
|
||||
// Don't ever hash size_t which varies with CPU architecture
|
||||
// Integer endianness intentionally disregarded
|
||||
uint32_t hashme = (uint32_t) m_index;
|
||||
|
||||
auto api = hasher_md5::get();
|
||||
hasher_md5_state state;
|
||||
api->initialize( state );
|
||||
api->process( state, &hashme, sizeof(hashme) );
|
||||
|
||||
// fb2k hasher_md5 API even provides a convenient method to return MD5 hashes cast to GUIDs for this.
|
||||
return api->get_result_guid( state );
|
||||
}
|
||||
bool get_description(pfc::string_base & out) {
|
||||
out = PFC_string_formatter() << "This is a test menu item #" << m_index << ".";
|
||||
return true;
|
||||
}
|
||||
private:
|
||||
const size_t m_index;
|
||||
};
|
||||
class sample_group : public mainmenu_node_group {
|
||||
public:
|
||||
sample_group() {
|
||||
m_children.resize(11);
|
||||
for( size_t walk = 0; walk < m_children.size(); ++ walk ) {
|
||||
mainmenu_node::ptr node;
|
||||
// Insert separators for odd items, commands for even
|
||||
if ( walk % 2 ) {
|
||||
node = fb2k::service_new<mainmenu_node_separator>();
|
||||
} else {
|
||||
auto cmdIndex = walk/2 + 1;
|
||||
node = fb2k::service_new<sample_command>( cmdIndex );
|
||||
}
|
||||
m_children[walk] = std::move(node);
|
||||
}
|
||||
}
|
||||
void get_display(pfc::string_base & text, t_uint32 & flags) override {
|
||||
flags = 0;
|
||||
text = "Dynamic menu test group";
|
||||
}
|
||||
t_size get_children_count() override {
|
||||
return m_children.size();
|
||||
}
|
||||
mainmenu_node::ptr get_child(t_size index) override {
|
||||
PFC_ASSERT( index < m_children.size() );
|
||||
return m_children[index];
|
||||
}
|
||||
private:
|
||||
std::vector<mainmenu_node::ptr> m_children;
|
||||
|
||||
};
|
||||
|
||||
class mainmenu_sample_dynamic : public mainmenu_commands_v2 {
|
||||
public:
|
||||
// mainmenu_commands_v2 methods
|
||||
t_uint32 get_command_count() override { return 1; }
|
||||
GUID get_command(t_uint32 p_index) override {return guid_menucommand;}
|
||||
void get_name(t_uint32 p_index,pfc::string_base & p_out) override {p_out = "Dynamic menu test";}
|
||||
|
||||
bool get_description(t_uint32 p_index,pfc::string_base & p_out) override {
|
||||
// Should not get here much
|
||||
p_out = "This is a dynamic menu command test.";
|
||||
return true;
|
||||
}
|
||||
GUID get_parent() override {return guid_mainmenu_group_id; }
|
||||
void execute(t_uint32 p_index,service_ptr_t<service_base> p_callback) override {
|
||||
// Should not get here, someone not aware of our dynamic status tried to invoke us?
|
||||
}
|
||||
|
||||
bool is_command_dynamic(t_uint32 index) override { return true; }
|
||||
mainmenu_node::ptr dynamic_instantiate(t_uint32 index) override {
|
||||
return fb2k::service_new<sample_group>();
|
||||
}
|
||||
|
||||
bool dynamic_execute(t_uint32 index, const GUID & subID, service_ptr_t<service_base> callback) override {
|
||||
// If your component provides a more efficient way to execute the command,
|
||||
// than doing full dynamic_instantiate() and walking all the dynamic items to find one with the matching identifier,
|
||||
// please implement it here.
|
||||
|
||||
// ... or just skip implementing this method entirely.
|
||||
return __super::dynamic_execute( index, subID, callback );
|
||||
}
|
||||
};
|
||||
|
||||
static service_factory_single_t<mainmenu_sample_dynamic> g_mainmenu_sample_dynamic;
|
||||
|
||||
} // namespace
|
||||
120
foobar2000/foo_sample/mainmenu.cpp
Normal file
120
foobar2000/foo_sample/mainmenu.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "stdafx.h"
|
||||
|
||||
#include "playback_stream_capture.h"
|
||||
|
||||
// I am foo_sample and these are *my* GUIDs
|
||||
// Make your own when reusing code or else
|
||||
static const GUID guid_mainmenu_group_id = { 0x44963e7a, 0x4b2a, 0x4588, { 0xb0, 0x17, 0xa8, 0x69, 0x18, 0xcb, 0x8a, 0xa5 } };
|
||||
static const GUID guid_test = { 0x7c4726df, 0x3b2d, 0x4c7c,{ 0xad, 0xe8, 0x43, 0xd8, 0x46, 0xbe, 0xce, 0xa8 } };
|
||||
static const GUID guid_playbackstate = { 0xbd880c51, 0xf0cc, 0x473f,{ 0x9d, 0x14, 0xa6, 0x6e, 0x8c, 0xed, 0x25, 0xae } };
|
||||
static const GUID guid_io = { 0xd380c333, 0xa72c, 0x4e1e,{ 0x97, 0xca, 0xed, 0x14, 0xeb, 0x93, 0x76, 0x23 } };
|
||||
static const GUID guid_listcontrol_advanced = { 0x27e29db0, 0x3079, 0x4ce0, { 0x8b, 0x4a, 0xa0, 0x78, 0xeb, 0x6, 0x56, 0x86 } };
|
||||
static const GUID guid_listcontrol_simple = { 0x34664996, 0x54cd, 0x48eb, { 0xa8, 0x20, 0x8f, 0x45, 0x7d, 0xcc, 0xff, 0xbb } };
|
||||
static const GUID guid_listcontrol_ownerdata = { 0xc6d23696, 0x4be5, 0x4daa, { 0xaf, 0xb2, 0x35, 0x14, 0xa, 0x47, 0xd2, 0xf9 } };
|
||||
static const GUID guid_playback_stream_capture = { 0x3d0f0f1a, 0x6b5f, 0x42e3, { 0xa4, 0x6d, 0x49, 0x1, 0x3, 0xf0, 0x54, 0xb2 } };
|
||||
|
||||
|
||||
static mainmenu_group_popup_factory g_mainmenu_group(guid_mainmenu_group_id, mainmenu_groups::file, mainmenu_commands::sort_priority_dontcare, "Sample component");
|
||||
|
||||
void RunPlaybackStateDemo(); //playback_state.cpp
|
||||
void RunIOTest(); // IO.cpp
|
||||
void RunListControlSimpleDemo(); // listcontrol-simple.cpp
|
||||
void RunListControlOwnerDataDemo(); // listcontrol-ownerdata.cpp
|
||||
void RunListControlAdvancedDemo(); // listcontrol-advanced.cpp
|
||||
|
||||
class mainmenu_commands_sample : public mainmenu_commands {
|
||||
public:
|
||||
enum {
|
||||
cmd_test = 0,
|
||||
cmd_playbackstate,
|
||||
cmd_io,
|
||||
cmd_listcontrol_simple,
|
||||
cmd_listcontrol_ownerdata,
|
||||
cmd_listcontrol_advanced,
|
||||
cmd_playback_stream_capture,
|
||||
cmd_total
|
||||
};
|
||||
t_uint32 get_command_count() override {
|
||||
return cmd_total;
|
||||
}
|
||||
GUID get_command(t_uint32 p_index) override {
|
||||
|
||||
switch(p_index) {
|
||||
case cmd_test: return guid_test;
|
||||
case cmd_playbackstate: return guid_playbackstate;
|
||||
case cmd_io: return guid_io;
|
||||
case cmd_listcontrol_simple: return guid_listcontrol_simple;
|
||||
case cmd_listcontrol_ownerdata: return guid_listcontrol_ownerdata;
|
||||
case cmd_listcontrol_advanced: return guid_listcontrol_advanced;
|
||||
case cmd_playback_stream_capture: return guid_playback_stream_capture;
|
||||
default: uBugCheck(); // should never happen unless somebody called us with invalid parameters - bail
|
||||
}
|
||||
}
|
||||
void get_name(t_uint32 p_index,pfc::string_base & p_out) override {
|
||||
switch(p_index) {
|
||||
case cmd_test: p_out = "Test command"; break;
|
||||
case cmd_playbackstate: p_out = "Playback state demo"; break;
|
||||
case cmd_io: p_out = "I/O test"; break;
|
||||
case cmd_listcontrol_simple: p_out = "Simple CListControl demo"; break;
|
||||
case cmd_listcontrol_ownerdata: p_out = "Owner-data CListControl demo"; break;
|
||||
case cmd_listcontrol_advanced: p_out = "Advanced CListControl demo"; break;
|
||||
case cmd_playback_stream_capture: p_out = "Playback stream capture demo"; break;
|
||||
default: uBugCheck(); // should never happen unless somebody called us with invalid parameters - bail
|
||||
}
|
||||
}
|
||||
bool get_description(t_uint32 p_index,pfc::string_base & p_out) override {
|
||||
switch(p_index) {
|
||||
case cmd_test: p_out = "This is a sample menu command."; return true;
|
||||
case cmd_playbackstate: p_out = "Opens the playback state demo dialog."; return true;
|
||||
case cmd_io: p_out = "Runs I/O test."; return true;
|
||||
case cmd_listcontrol_simple: p_out = "Runs Simple CListControl demo."; return true;
|
||||
case cmd_listcontrol_ownerdata: p_out = "Runs Owner Data CListControl demo."; return true;
|
||||
case cmd_listcontrol_advanced: p_out = "Runs Advanced CListControl demo."; return true;
|
||||
case cmd_playback_stream_capture: p_out = "Toggles playback stream capture operation."; return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
GUID get_parent() override {
|
||||
return guid_mainmenu_group_id;
|
||||
}
|
||||
void execute(t_uint32 p_index,service_ptr_t<service_base> p_callback) override {
|
||||
switch(p_index) {
|
||||
case cmd_test:
|
||||
popup_message::g_show("This is a sample menu command.", "Blah");
|
||||
break;
|
||||
case cmd_playbackstate:
|
||||
RunPlaybackStateDemo();
|
||||
break;
|
||||
case cmd_io:
|
||||
RunIOTest();
|
||||
break;
|
||||
case cmd_listcontrol_simple:
|
||||
RunListControlSimpleDemo();
|
||||
break;
|
||||
case cmd_listcontrol_ownerdata:
|
||||
RunListControlOwnerDataDemo();
|
||||
break;
|
||||
case cmd_listcontrol_advanced:
|
||||
RunListControlAdvancedDemo();
|
||||
break;
|
||||
case cmd_playback_stream_capture:
|
||||
ToggleCapture();
|
||||
break;
|
||||
default:
|
||||
uBugCheck(); // should never happen unless somebody called us with invalid parameters - bail
|
||||
}
|
||||
}
|
||||
bool get_display(t_uint32 p_index,pfc::string_base & p_text,t_uint32 & p_flags) override {
|
||||
// OPTIONAL method
|
||||
bool rv = __super::get_display(p_index, p_text, p_flags);
|
||||
if (rv) switch(p_index) {
|
||||
case cmd_playback_stream_capture:
|
||||
// Add checkmark if capture is in progress
|
||||
if ( IsCaptureRunning() ) p_flags |= flag_checked;
|
||||
break;
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
};
|
||||
|
||||
static mainmenu_commands_factory_t<mainmenu_commands_sample> g_mainmenu_commands_sample_factory;
|
||||
155
foobar2000/foo_sample/playback_state.cpp
Normal file
155
foobar2000/foo_sample/playback_state.cpp
Normal file
@@ -0,0 +1,155 @@
|
||||
#include "stdafx.h"
|
||||
#include "resource.h"
|
||||
#include <helpers/WindowPositionUtils.h>
|
||||
#include <helpers/atl-misc.h>
|
||||
|
||||
class CPlaybackStateDemo : public CDialogImpl<CPlaybackStateDemo>, private play_callback_impl_base {
|
||||
public:
|
||||
enum {IDD = IDD_PLAYBACK_STATE};
|
||||
|
||||
BEGIN_MSG_MAP_EX(CPlaybackStateDemo)
|
||||
MSG_WM_INITDIALOG(OnInitDialog)
|
||||
COMMAND_HANDLER_EX(IDC_PATTERN, EN_CHANGE, OnPatternChange)
|
||||
COMMAND_HANDLER_EX(IDCANCEL, BN_CLICKED, OnCancel)
|
||||
COMMAND_HANDLER_EX(IDC_PLAY, BN_CLICKED, OnPlayClicked)
|
||||
COMMAND_HANDLER_EX(IDC_PAUSE, BN_CLICKED, OnPauseClicked)
|
||||
COMMAND_HANDLER_EX(IDC_STOP, BN_CLICKED, OnStopClicked)
|
||||
COMMAND_HANDLER_EX(IDC_PREV, BN_CLICKED, OnPrevClicked)
|
||||
COMMAND_HANDLER_EX(IDC_NEXT, BN_CLICKED, OnNextClicked)
|
||||
COMMAND_HANDLER_EX(IDC_RAND, BN_CLICKED, OnRandClicked)
|
||||
MSG_WM_CONTEXTMENU(OnContextMenu)
|
||||
END_MSG_MAP()
|
||||
private:
|
||||
|
||||
// Playback callback methods.
|
||||
void on_playback_starting(play_control::t_track_command p_command,bool p_paused) {update();}
|
||||
void on_playback_new_track(metadb_handle_ptr p_track) {update();}
|
||||
void on_playback_stop(play_control::t_stop_reason p_reason) {update();}
|
||||
void on_playback_seek(double p_time) {update();}
|
||||
void on_playback_pause(bool p_state) {update();}
|
||||
void on_playback_edited(metadb_handle_ptr p_track) {update();}
|
||||
void on_playback_dynamic_info(const file_info & p_info) {update();}
|
||||
void on_playback_dynamic_info_track(const file_info & p_info) {update();}
|
||||
void on_playback_time(double p_time) {update();}
|
||||
void on_volume_change(float p_new_val) {}
|
||||
|
||||
void update();
|
||||
|
||||
void OnPatternChange(UINT, int, CWindow);
|
||||
void OnCancel(UINT, int, CWindow);
|
||||
|
||||
void OnPlayClicked(UINT, int, CWindow) {m_playback_control->start();}
|
||||
void OnStopClicked(UINT, int, CWindow) {m_playback_control->stop();}
|
||||
void OnPauseClicked(UINT, int, CWindow) {m_playback_control->toggle_pause();}
|
||||
void OnPrevClicked(UINT, int, CWindow) {m_playback_control->start(playback_control::track_command_prev);}
|
||||
void OnNextClicked(UINT, int, CWindow) {m_playback_control->start(playback_control::track_command_next);}
|
||||
void OnRandClicked(UINT, int, CWindow) {m_playback_control->start(playback_control::track_command_rand);}
|
||||
|
||||
void OnContextMenu(CWindow wnd, CPoint point);
|
||||
|
||||
BOOL OnInitDialog(CWindow, LPARAM);
|
||||
|
||||
titleformat_object::ptr m_script;
|
||||
|
||||
static_api_ptr_t<playback_control> m_playback_control;
|
||||
};
|
||||
|
||||
void CPlaybackStateDemo::OnCancel(UINT, int, CWindow) {
|
||||
DestroyWindow();
|
||||
}
|
||||
|
||||
void CPlaybackStateDemo::OnPatternChange(UINT, int, CWindow) {
|
||||
m_script.release(); // pattern has changed, force script recompilation
|
||||
update();
|
||||
}
|
||||
|
||||
BOOL CPlaybackStateDemo::OnInitDialog(CWindow, LPARAM) {
|
||||
update();
|
||||
SetDlgItemText(IDC_PATTERN, _T("%codec% | %bitrate% kbps | %samplerate% Hz | %channels% | %playback_time%[ / %length%]$if(%ispaused%, | paused,)"));
|
||||
::ShowWindowCentered(*this,GetParent()); // Function declared in SDK helpers.
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CPlaybackStateDemo::update() {
|
||||
if (m_script.is_empty()) {
|
||||
pfc::string8 pattern;
|
||||
uGetDlgItemText(*this, IDC_PATTERN, pattern);
|
||||
static_api_ptr_t<titleformat_compiler>()->compile_safe_ex(m_script, pattern);
|
||||
}
|
||||
pfc::string_formatter state;
|
||||
if (m_playback_control->playback_format_title(NULL, state, m_script, NULL, playback_control::display_level_all)) {
|
||||
//Succeeded already.
|
||||
} else if (m_playback_control->is_playing()) {
|
||||
//Starting playback but not done opening the first track yet.
|
||||
state = "Opening...";
|
||||
} else {
|
||||
state = "Stopped.";
|
||||
}
|
||||
uSetDlgItemText(*this, IDC_STATE, state);
|
||||
}
|
||||
|
||||
void CPlaybackStateDemo::OnContextMenu(CWindow wnd, CPoint point) {
|
||||
try {
|
||||
if (wnd == GetDlgItem(IDC_CONTEXTMENU)) {
|
||||
|
||||
// handle the context menu key case - center the menu
|
||||
if (point == CPoint(-1, -1)) {
|
||||
CRect rc;
|
||||
WIN32_OP(wnd.GetWindowRect(&rc));
|
||||
point = rc.CenterPoint();
|
||||
}
|
||||
|
||||
metadb_handle_list items;
|
||||
|
||||
{ // note: we would normally just use contextmenu_manager::init_context_now_playing(), but we go the "make the list ourselves" route to demonstrate how to invoke the menu for arbitrary items.
|
||||
metadb_handle_ptr item;
|
||||
if (m_playback_control->get_now_playing(item)) items += item;
|
||||
}
|
||||
|
||||
CMenuDescriptionHybrid menudesc(*this); //this class manages all the voodoo necessary for descriptions of our menu items to show in the status bar.
|
||||
|
||||
static_api_ptr_t<contextmenu_manager> api;
|
||||
CMenu menu;
|
||||
WIN32_OP(menu.CreatePopupMenu());
|
||||
enum {
|
||||
ID_TESTCMD = 1,
|
||||
ID_CM_BASE,
|
||||
};
|
||||
menu.AppendMenu(MF_STRING, ID_TESTCMD, _T("Test command"));
|
||||
menudesc.Set(ID_TESTCMD, "This is a test command.");
|
||||
menu.AppendMenu(MF_SEPARATOR);
|
||||
|
||||
if (items.get_count() > 0) {
|
||||
api->init_context(items, 0);
|
||||
api->win32_build_menu(menu, ID_CM_BASE, ~0);
|
||||
menudesc.SetCM(api.get_ptr(), ID_CM_BASE, ~0);
|
||||
} else {
|
||||
menu.AppendMenu(MF_STRING|MF_GRAYED|MF_DISABLED, (UINT)0, _T("No items selected"));
|
||||
}
|
||||
|
||||
int cmd = menu.TrackPopupMenu(TPM_RIGHTBUTTON|TPM_NONOTIFY|TPM_RETURNCMD,point.x,point.y,menudesc,0);
|
||||
if (cmd > 0) {
|
||||
if (cmd >= ID_CM_BASE) {
|
||||
api->execute_by_id(cmd - ID_CM_BASE);
|
||||
} else switch(cmd) {
|
||||
case ID_TESTCMD:
|
||||
popup_message::g_show("Blah!", "Test");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} catch(std::exception const & e) {
|
||||
console::complain("Context menu failure", e); //rare
|
||||
}
|
||||
}
|
||||
|
||||
void RunPlaybackStateDemo() {
|
||||
try {
|
||||
// ImplementModelessTracking registers our dialog to receive dialog messages thru main app loop's IsDialogMessage().
|
||||
// CWindowAutoLifetime creates the window in the constructor (taking the parent window as a parameter) and deletes the object when the window has been destroyed (through WTL's OnFinalMessage).
|
||||
new CWindowAutoLifetime<ImplementModelessTracking<CPlaybackStateDemo> >(core_api::get_main_window());
|
||||
} catch(std::exception const & e) {
|
||||
popup_message::g_complain("Dialog creation failure", e);
|
||||
}
|
||||
}
|
||||
121
foobar2000/foo_sample/playback_stream_capture.cpp
Normal file
121
foobar2000/foo_sample/playback_stream_capture.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
#include "stdafx.h"
|
||||
#include <helpers/writer_wav.h>
|
||||
|
||||
namespace {
|
||||
// private classes in anon namespace
|
||||
|
||||
typedef CWavWriter wav_writer;
|
||||
typedef wavWriterSetup_t wav_writer_setup;
|
||||
|
||||
static pfc::string8 g_outputPath;
|
||||
static wav_writer g_wav_writer;
|
||||
|
||||
class playback_stream_capture_callback_impl : public playback_stream_capture_callback {
|
||||
public:
|
||||
void on_chunk(const audio_chunk & chunk) override {
|
||||
PFC_ASSERT(core_api::is_main_thread());
|
||||
|
||||
try {
|
||||
// writing files in main thread is not pretty, but good enough for our demo
|
||||
|
||||
auto & abort = fb2k::noAbort;
|
||||
|
||||
if (g_wav_writer.is_open() && g_wav_writer.get_spec() != chunk.get_spec()) {
|
||||
g_wav_writer.finalize(abort);
|
||||
}
|
||||
if (!g_wav_writer.is_open() && ! core_api::is_shutting_down() ) {
|
||||
wav_writer_setup setup; setup.initialize(chunk, 16, false, false);
|
||||
|
||||
GUID g; CoCreateGuid(&g);
|
||||
pfc::string_formatter fn;
|
||||
fn << "capture-" << pfc::print_guid(g) << ".wav";
|
||||
pfc::string_formatter path = g_outputPath;
|
||||
path.add_filename( fn ); // pretty method to add file path components with auto inserted delimiter
|
||||
g_wav_writer.open(path, setup, abort);
|
||||
}
|
||||
g_wav_writer.write(chunk, abort);
|
||||
} catch(std::exception const & e) {
|
||||
FB2K_console_formatter() << "Playback stream capture error: " << e;
|
||||
// FIX ME handle this in a pretty manner, likely inaccessible output folder or out of disk space
|
||||
}
|
||||
}
|
||||
};
|
||||
static playback_stream_capture_callback_impl g_callback;
|
||||
static bool g_active = false;
|
||||
|
||||
static void FlushCapture() {
|
||||
if (g_active) {
|
||||
g_wav_writer.finalize(fb2k::noAbort);
|
||||
}
|
||||
}
|
||||
static void StopCapture() {
|
||||
if ( g_active ) {
|
||||
g_active = false;
|
||||
playback_stream_capture::get()->remove_callback(&g_callback);
|
||||
g_wav_writer.finalize(fb2k::noAbort);
|
||||
}
|
||||
}
|
||||
static void StartCapture() {
|
||||
PFC_ASSERT( g_outputPath.length() > 0 );
|
||||
if (!g_active && !core_api::is_shutting_down()) {
|
||||
g_active = true;
|
||||
playback_stream_capture::get()->add_callback(&g_callback);
|
||||
}
|
||||
}
|
||||
|
||||
// Forcibly stop capture when fb2k is shutting down
|
||||
class initquit_psc : public initquit {
|
||||
public:
|
||||
void on_quit() override {
|
||||
PFC_ASSERT( core_api::is_shutting_down() );
|
||||
StopCapture();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle playback stop events to split output WAV files
|
||||
class play_callback_psc : public play_callback_static {
|
||||
public:
|
||||
unsigned get_flags() override {
|
||||
return flag_on_playback_stop;
|
||||
}
|
||||
void on_playback_stop(play_control::t_stop_reason p_reason) override {
|
||||
// Terminate the current stream
|
||||
FlushCapture();
|
||||
}
|
||||
void on_playback_starting(play_control::t_track_command p_command,bool p_paused) override {}
|
||||
void on_playback_new_track(metadb_handle_ptr p_track) override {}
|
||||
void on_playback_seek(double p_time) override {}
|
||||
void on_playback_pause(bool p_state) override {}
|
||||
void on_playback_edited(metadb_handle_ptr p_track) override {}
|
||||
void on_playback_dynamic_info(const file_info & p_info) override {}
|
||||
void on_playback_dynamic_info_track(const file_info & p_info) override {}
|
||||
void on_playback_time(double p_time) override {}
|
||||
void on_volume_change(float p_new_val) override {}
|
||||
};
|
||||
|
||||
// pretty modern macro for service_factory_single_t<>
|
||||
FB2K_SERVICE_FACTORY( initquit_psc );
|
||||
FB2K_SERVICE_FACTORY( play_callback_psc );
|
||||
}
|
||||
|
||||
void ToggleCapture() {
|
||||
// Block modal dialog recursions.
|
||||
// Folder picker below is a modal dialog, don't ever call it if there's another modal dialog in progress.
|
||||
// Also prevents this function from recursing into itself if someone manages to hit the menu item while already picking folder.
|
||||
// This will bump whatever modal dialog already exists, so the user has some idea why this was refused.
|
||||
if ( !ModalDialogPrologue() ) return;
|
||||
|
||||
if (g_active) {
|
||||
StopCapture();
|
||||
} else {
|
||||
const HWND wndParent = core_api::get_main_window();
|
||||
modal_dialog_scope scope(wndParent); // we can't have a handle to the modal dialog, but parent handle is good enough
|
||||
if (uBrowseForFolder(wndParent, "Choose output folder", g_outputPath)) {
|
||||
StartCapture();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IsCaptureRunning() {
|
||||
return g_active;
|
||||
}
|
||||
4
foobar2000/foo_sample/playback_stream_capture.h
Normal file
4
foobar2000/foo_sample/playback_stream_capture.h
Normal file
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
void ToggleCapture();
|
||||
bool IsCaptureRunning();
|
||||
111
foobar2000/foo_sample/preferences.cpp
Normal file
111
foobar2000/foo_sample/preferences.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
#include "stdafx.h"
|
||||
#include "resource.h"
|
||||
#include <helpers/atl-misc.h>
|
||||
|
||||
// Sample preferences interface: two meaningless configuration settings accessible through a preferences page and one accessible through advanced preferences.
|
||||
|
||||
|
||||
// These GUIDs identify the variables within our component's configuration file.
|
||||
static const GUID guid_cfg_bogoSetting1 = { 0xbd5c777, 0x735c, 0x440d, { 0x8c, 0x71, 0x49, 0xb6, 0xac, 0xff, 0xce, 0xb8 } };
|
||||
static const GUID guid_cfg_bogoSetting2 = { 0x752f1186, 0x9f61, 0x4f91, { 0xb3, 0xee, 0x2f, 0x25, 0xb1, 0x24, 0x83, 0x5d } };
|
||||
|
||||
// This GUID identifies our Advanced Preferences branch (replace with your own when reusing code).
|
||||
static const GUID guid_advconfig_branch = { 0x28564ced, 0x4abf, 0x4f0c, { 0xa4, 0x43, 0x98, 0xda, 0x88, 0xe2, 0xcd, 0x39 } };
|
||||
// This GUID identifies our Advanced Preferences setting (replace with your own when reusing code) as well as this setting's storage within our component's configuration file.
|
||||
static const GUID guid_cfg_bogoSetting3 = { 0xf7008963, 0xed60, 0x4084, { 0xa8, 0x5d, 0xd1, 0xcd, 0xc5, 0x51, 0x22, 0xca } };
|
||||
|
||||
|
||||
enum {
|
||||
default_cfg_bogoSetting1 = 1337,
|
||||
default_cfg_bogoSetting2 = 666,
|
||||
default_cfg_bogoSetting3 = 42,
|
||||
};
|
||||
|
||||
static cfg_uint cfg_bogoSetting1(guid_cfg_bogoSetting1, default_cfg_bogoSetting1), cfg_bogoSetting2(guid_cfg_bogoSetting2, default_cfg_bogoSetting2);
|
||||
|
||||
static advconfig_branch_factory g_advconfigBranch("Sample Component", guid_advconfig_branch, advconfig_branch::guid_branch_tools, 0);
|
||||
static advconfig_integer_factory cfg_bogoSetting3("Bogo setting 3", guid_cfg_bogoSetting3, guid_advconfig_branch, 0, default_cfg_bogoSetting3, 0 /*minimum value*/, 9999 /*maximum value*/);
|
||||
|
||||
class CMyPreferences : public CDialogImpl<CMyPreferences>, public preferences_page_instance {
|
||||
public:
|
||||
//Constructor - invoked by preferences_page_impl helpers - don't do Create() in here, preferences_page_impl does this for us
|
||||
CMyPreferences(preferences_page_callback::ptr callback) : m_callback(callback) {}
|
||||
|
||||
//Note that we don't bother doing anything regarding destruction of our class.
|
||||
//The host ensures that our dialog is destroyed first, then the last reference to our preferences_page_instance object is released, causing our object to be deleted.
|
||||
|
||||
|
||||
//dialog resource ID
|
||||
enum {IDD = IDD_MYPREFERENCES};
|
||||
// preferences_page_instance methods (not all of them - get_wnd() is supplied by preferences_page_impl helpers)
|
||||
t_uint32 get_state();
|
||||
void apply();
|
||||
void reset();
|
||||
|
||||
//WTL message map
|
||||
BEGIN_MSG_MAP_EX(CMyPreferences)
|
||||
MSG_WM_INITDIALOG(OnInitDialog)
|
||||
COMMAND_HANDLER_EX(IDC_BOGO1, EN_CHANGE, OnEditChange)
|
||||
COMMAND_HANDLER_EX(IDC_BOGO2, EN_CHANGE, OnEditChange)
|
||||
END_MSG_MAP()
|
||||
private:
|
||||
BOOL OnInitDialog(CWindow, LPARAM);
|
||||
void OnEditChange(UINT, int, CWindow);
|
||||
bool HasChanged();
|
||||
void OnChanged();
|
||||
|
||||
const preferences_page_callback::ptr m_callback;
|
||||
};
|
||||
|
||||
BOOL CMyPreferences::OnInitDialog(CWindow, LPARAM) {
|
||||
SetDlgItemInt(IDC_BOGO1, cfg_bogoSetting1, FALSE);
|
||||
SetDlgItemInt(IDC_BOGO2, cfg_bogoSetting2, FALSE);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void CMyPreferences::OnEditChange(UINT, int, CWindow) {
|
||||
// not much to do here
|
||||
OnChanged();
|
||||
}
|
||||
|
||||
t_uint32 CMyPreferences::get_state() {
|
||||
t_uint32 state = preferences_state::resettable;
|
||||
if (HasChanged()) state |= preferences_state::changed;
|
||||
return state;
|
||||
}
|
||||
|
||||
void CMyPreferences::reset() {
|
||||
SetDlgItemInt(IDC_BOGO1, default_cfg_bogoSetting1, FALSE);
|
||||
SetDlgItemInt(IDC_BOGO2, default_cfg_bogoSetting2, FALSE);
|
||||
OnChanged();
|
||||
}
|
||||
|
||||
void CMyPreferences::apply() {
|
||||
cfg_bogoSetting1 = GetDlgItemInt(IDC_BOGO1, NULL, FALSE);
|
||||
cfg_bogoSetting2 = GetDlgItemInt(IDC_BOGO2, NULL, FALSE);
|
||||
|
||||
OnChanged(); //our dialog content has not changed but the flags have - our currently shown values now match the settings so the apply button can be disabled
|
||||
}
|
||||
|
||||
bool CMyPreferences::HasChanged() {
|
||||
//returns whether our dialog content is different from the current configuration (whether the apply button should be enabled or not)
|
||||
return GetDlgItemInt(IDC_BOGO1, NULL, FALSE) != cfg_bogoSetting1 || GetDlgItemInt(IDC_BOGO2, NULL, FALSE) != cfg_bogoSetting2;
|
||||
}
|
||||
void CMyPreferences::OnChanged() {
|
||||
//tell the host that our state has changed to enable/disable the apply button appropriately.
|
||||
m_callback->on_state_changed();
|
||||
}
|
||||
|
||||
class preferences_page_myimpl : public preferences_page_impl<CMyPreferences> {
|
||||
// preferences_page_impl<> helper deals with instantiation of our dialog; inherits from preferences_page_v3.
|
||||
public:
|
||||
const char * get_name() {return "Sample Component";}
|
||||
GUID get_guid() {
|
||||
// This is our GUID. Replace with your own when reusing the code.
|
||||
static const GUID guid = { 0x7702c93e, 0x24dc, 0x48ed, { 0x8d, 0xb1, 0x3f, 0x27, 0xb3, 0x8c, 0x7c, 0xc9 } };
|
||||
return guid;
|
||||
}
|
||||
GUID get_parent_guid() {return guid_tools;}
|
||||
};
|
||||
|
||||
static preferences_page_factory_t<preferences_page_myimpl> g_preferences_page_myimpl_factory;
|
||||
592
foobar2000/foo_sample/rating.cpp
Normal file
592
foobar2000/foo_sample/rating.cpp
Normal file
@@ -0,0 +1,592 @@
|
||||
#include "stdafx.h"
|
||||
|
||||
|
||||
/*
|
||||
========================================================================
|
||||
Sample implementation of metadb_index_client and a rating database.
|
||||
========================================================================
|
||||
|
||||
The rating data is all maintained by metadb backend, we only present and alter it when asked to.
|
||||
Relevant classes:
|
||||
metadb_index_client_impl - turns track info into a database key to which our data gets pinned.
|
||||
init_stage_callback_impl - initializes ourselves at the proper moment of the app lifetime.
|
||||
initquit_imp - clean up cached objects on app shutdown
|
||||
metadb_display_field_provider_impl - publishes our %foo_sample_...% fields via title formatting.
|
||||
contextmenu_rating - context menu command to cycle rating values.
|
||||
mainmenu_rating - main menu command to show a dump of all present ratings.
|
||||
track_property_provider_impl - serves info for the Properties dialog
|
||||
*/
|
||||
|
||||
namespace {
|
||||
|
||||
// I am foo_sample and these are *my* GUIDs.
|
||||
// Replace with your own when reusing.
|
||||
// Always recreate guid_foo_sample_rating_index if your metadb_index_client_impl hashing semantics changed or else you run into inconsistent/nonsensical data.
|
||||
static const GUID guid_foo_sample_track_rating_index = { 0x88cf3f09, 0x26a8, 0x42ef,{ 0xb7, 0xb8, 0x42, 0x21, 0xb9, 0x62, 0x26, 0x78 } };
|
||||
static const GUID guid_foo_sample_album_rating_index = { 0xd94ba576, 0x7fab, 0x4f1b,{ 0xbe, 0x5e, 0x4f, 0x8e, 0x9d, 0x5f, 0x30, 0xf1 } };
|
||||
static const GUID guid_foo_sample_rating_contextmenu1 = { 0x5d71c93, 0x5d38, 0x4e63,{ 0x97, 0x66, 0x8f, 0xb7, 0x6d, 0xc7, 0xc5, 0x9e } };
|
||||
static const GUID guid_foo_sample_rating_contextmenu2 = { 0xf3972846, 0x7c32, 0x44fa,{ 0xbd, 0xa, 0x68, 0x86, 0x65, 0x69, 0x4b, 0x7d } };
|
||||
static const GUID guid_foo_sample_rating_contextmenu3 = { 0x67a6d984, 0xe499, 0x4f86,{ 0xb9, 0xcb, 0x66, 0x8e, 0x59, 0xb8, 0xd0, 0xe6 } };
|
||||
static const GUID guid_foo_sample_rating_contextmenu4 = { 0x4541dfa5, 0x7976, 0x43aa,{ 0xb9, 0x73, 0x10, 0xc3, 0x26, 0x55, 0x5a, 0x5c } };
|
||||
|
||||
static const GUID guid_foo_sample_contextmenu_group = { 0x572de7f4, 0xcbdf, 0x479a,{ 0x97, 0x26, 0xa, 0xb0, 0x97, 0x47, 0x69, 0xe3 } };
|
||||
static const GUID guid_foo_sample_rating_mainmenu = { 0x53327baa, 0xbaa4, 0x478e,{ 0x87, 0x24, 0xf7, 0x38, 0x4, 0x15, 0xf7, 0x27 } };
|
||||
static const GUID guid_foo_sample_mainmenu_group = { 0x44963e7a, 0x4b2a, 0x4588,{ 0xb0, 0x17, 0xa8, 0x69, 0x18, 0xcb, 0x8a, 0xa5 } };
|
||||
|
||||
// Patterns by which we pin our data to.
|
||||
// If multiple songs in the library evaluate to the same string, they will be considered the same by our component,
|
||||
// and data applied to one will also show up with the rest.
|
||||
// Always recreate relevant index GUIDs if you change these
|
||||
static const char strTrackRatingPinTo[] = "%artist% - %title%";
|
||||
static const char strAlbumRatingPinTo[] = "%album artist% - %album%";
|
||||
|
||||
|
||||
// Our group in Properties dialog / Details tab, see track_property_provider_impl
|
||||
static const char strPropertiesGroup[] = "Sample Component";
|
||||
|
||||
// Retain pinned data for four weeks if there are no matching items in library
|
||||
static const t_filetimestamp retentionPeriod = system_time_periods::week * 4;
|
||||
|
||||
// A class that turns metadata + location info into hashes to which our data gets pinned by the backend.
|
||||
class metadb_index_client_impl : public metadb_index_client {
|
||||
public:
|
||||
metadb_index_client_impl( const char * pinTo ) {
|
||||
static_api_ptr_t<titleformat_compiler>()->compile_force(m_keyObj, pinTo);
|
||||
}
|
||||
|
||||
metadb_index_hash transform(const file_info & info, const playable_location & location) {
|
||||
pfc::string_formatter str;
|
||||
m_keyObj->run_simple( location, &info, str );
|
||||
// Make MD5 hash of the string, then reduce it to 64-bit metadb_index_hash
|
||||
return static_api_ptr_t<hasher_md5>()->process_single_string( str ).xorHalve();
|
||||
}
|
||||
private:
|
||||
titleformat_object::ptr m_keyObj;
|
||||
};
|
||||
|
||||
static metadb_index_client_impl * clientByGUID( const GUID & guid ) {
|
||||
// Static instances, never destroyed (deallocated with the process), created first time we get here
|
||||
// Using service_impl_single_t, reference counting disabled
|
||||
// This is somewhat ugly, operating on raw pointers instead of service_ptr, but OK for this purpose
|
||||
static metadb_index_client_impl * g_clientTrack = new service_impl_single_t<metadb_index_client_impl>(strTrackRatingPinTo);
|
||||
static metadb_index_client_impl * g_clientAlbum = new service_impl_single_t<metadb_index_client_impl>(strAlbumRatingPinTo);
|
||||
|
||||
PFC_ASSERT( guid == guid_foo_sample_album_rating_index || guid == guid_foo_sample_track_rating_index );
|
||||
return (guid == guid_foo_sample_album_rating_index ) ? g_clientAlbum : g_clientTrack;
|
||||
}
|
||||
|
||||
// Static cached ptr to metadb_index_manager
|
||||
// Cached because we'll be calling it a lot on per-track basis, let's not pass it everywhere to low level functions
|
||||
// Obtaining the pointer from core is reasonably efficient - log(n) to the number of known service classes, but not good enough for something potentially called hundreds of times
|
||||
static metadb_index_manager::ptr g_cachedAPI;
|
||||
static metadb_index_manager::ptr theAPI() {
|
||||
auto ret = g_cachedAPI;
|
||||
if ( ret.is_empty() ) ret = metadb_index_manager::get(); // since fb2k SDK v1.4, core API interfaces have a static get() method
|
||||
return ret;
|
||||
}
|
||||
|
||||
// An init_stage_callback to hook ourselves into the metadb
|
||||
// We need to do this properly early to prevent dispatch_global_refresh() from new fields that we added from hammering playlists etc
|
||||
class init_stage_callback_impl : public init_stage_callback {
|
||||
public:
|
||||
void on_init_stage(t_uint32 stage) {
|
||||
if (stage == init_stages::before_config_read) {
|
||||
auto api = metadb_index_manager::get();
|
||||
g_cachedAPI = api;
|
||||
// Important, handle the exceptions here!
|
||||
// This will fail if the files holding our data are somehow corrupted.
|
||||
try {
|
||||
api->add(clientByGUID(guid_foo_sample_track_rating_index), guid_foo_sample_track_rating_index, retentionPeriod);
|
||||
api->add(clientByGUID(guid_foo_sample_album_rating_index), guid_foo_sample_album_rating_index, retentionPeriod);
|
||||
} catch (std::exception const & e) {
|
||||
api->remove(guid_foo_sample_track_rating_index);
|
||||
api->remove(guid_foo_sample_album_rating_index);
|
||||
FB2K_console_formatter() << "[foo_sample rating] Critical initialization failure: " << e;
|
||||
return;
|
||||
}
|
||||
api->dispatch_global_refresh();
|
||||
}
|
||||
}
|
||||
};
|
||||
class initquit_impl : public initquit {
|
||||
public:
|
||||
void on_quit() {
|
||||
// Cleanly kill g_cachedAPI before reaching static object destructors or else
|
||||
g_cachedAPI.release();
|
||||
}
|
||||
};
|
||||
|
||||
static service_factory_single_t<init_stage_callback_impl> g_init_stage_callback_impl;
|
||||
static service_factory_single_t<initquit_impl> g_initquit_impl;
|
||||
|
||||
typedef uint32_t rating_t;
|
||||
static const rating_t rating_invalid = 0;
|
||||
static const rating_t rating_max = 5;
|
||||
|
||||
struct record_t {
|
||||
rating_t m_rating = rating_invalid;
|
||||
pfc::string8 m_comment;
|
||||
};
|
||||
|
||||
static record_t record_get(const GUID & indexID, metadb_index_hash hash) {
|
||||
mem_block_container_impl temp; // this will receive our BLOB
|
||||
theAPI()->get_user_data( indexID, hash, temp );
|
||||
if ( temp.get_size() > 0 ) {
|
||||
try {
|
||||
// Parse the BLOB using stream formatters
|
||||
stream_reader_formatter_simple_ref< /* using big endian data? nope */ false > reader(temp.get_ptr(), temp.get_size());
|
||||
|
||||
record_t ret;
|
||||
reader >> ret.m_rating; // little endian uint32 got
|
||||
|
||||
if ( reader.get_remaining() > 0 ) {
|
||||
// more data left in the stream?
|
||||
// note that this is a stream_reader_formatter_simple_ref method, regular stream formatters do not know the size or seek around, only read the stream sequentially
|
||||
reader >> ret.m_comment; // this reads uint32 prefix indicating string size in bytes, then the actual string in UTF-8 characters }
|
||||
} // otherwise we leave the string empty
|
||||
|
||||
// if we attempted to read past the EOF, we'd land in the exception_io_data handler below
|
||||
|
||||
return ret;
|
||||
|
||||
} catch (exception_io_data) {
|
||||
// we get here as a result of stream formatter data error
|
||||
// fall thru to return a blank record
|
||||
}
|
||||
}
|
||||
return record_t();
|
||||
}
|
||||
|
||||
void record_set( const GUID & indexID, metadb_index_hash hash, const record_t & record) {
|
||||
|
||||
stream_writer_formatter_simple< /* using bing endian data? nope */ false > writer;
|
||||
writer << record.m_rating;
|
||||
if ( record.m_comment.length() > 0 ) {
|
||||
// bother with this only if the comment is not blank
|
||||
writer << record.m_comment; // uint32 size + UTF-8 bytes
|
||||
}
|
||||
|
||||
theAPI()->set_user_data( indexID, hash, writer.m_buffer.get_ptr(), writer.m_buffer.get_size() );
|
||||
}
|
||||
|
||||
static rating_t rating_get( const GUID & indexID, metadb_index_hash hash) {
|
||||
return record_get(indexID, hash).m_rating;
|
||||
}
|
||||
|
||||
|
||||
// Returns true if the value was actually changed
|
||||
static bool rating_set( const GUID & indexID, metadb_index_hash hash, rating_t rating) {
|
||||
bool bChanged = false;
|
||||
auto rec = record_get(indexID, hash);
|
||||
if ( rec.m_rating != rating ) {
|
||||
rec.m_rating = rating;
|
||||
record_set( indexID, hash, rec);
|
||||
bChanged = true;
|
||||
}
|
||||
return bChanged;
|
||||
}
|
||||
|
||||
static bool comment_set( const GUID & indexID, metadb_index_hash hash, const char * strComment ) {
|
||||
auto rec = record_get(indexID, hash );
|
||||
bool bChanged = false;
|
||||
if ( ! rec.m_comment.equals( strComment ) ) {
|
||||
rec.m_comment = strComment;
|
||||
record_set(indexID, hash, rec);
|
||||
bChanged = true;
|
||||
}
|
||||
return bChanged;
|
||||
}
|
||||
|
||||
// Provider of our title formatting fields.
|
||||
class metadb_display_field_provider_impl : public metadb_display_field_provider {
|
||||
public:
|
||||
t_uint32 get_field_count() {
|
||||
return 6;
|
||||
}
|
||||
void get_field_name(t_uint32 index, pfc::string_base & out) {
|
||||
PFC_ASSERT(index < get_field_count());
|
||||
switch(index) {
|
||||
case 0:
|
||||
out = "foo_sample_track_rating"; break;
|
||||
case 1:
|
||||
out = "foo_sample_album_rating"; break;
|
||||
case 2:
|
||||
out = "foo_sample_track_comment"; break;
|
||||
case 3:
|
||||
out = "foo_sample_album_comment"; break;
|
||||
case 4:
|
||||
out = "foo_sample_track_hash"; break;
|
||||
case 5:
|
||||
out = "foo_sample_album_hash"; break;
|
||||
default:
|
||||
PFC_ASSERT(!"Should never get here");
|
||||
}
|
||||
}
|
||||
|
||||
bool process_field(t_uint32 index, metadb_handle * handle, titleformat_text_out * out) {
|
||||
PFC_ASSERT( index < get_field_count() );
|
||||
|
||||
const GUID whichID = ((index%2) == 1) ? guid_foo_sample_album_rating_index : guid_foo_sample_track_rating_index;
|
||||
|
||||
record_t rec;
|
||||
metadb_index_hash hash;
|
||||
if (!clientByGUID(whichID)->hashHandle(handle, hash)) return false;
|
||||
|
||||
if ( index < 4 ) {
|
||||
rec = record_get(whichID, hash);
|
||||
}
|
||||
|
||||
|
||||
if ( index < 2 ) {
|
||||
// rating
|
||||
|
||||
if (rec.m_rating == rating_invalid) return false;
|
||||
|
||||
out->write_int(titleformat_inputtypes::meta, rec.m_rating);
|
||||
|
||||
return true;
|
||||
} else if ( index < 4 ) {
|
||||
// comment
|
||||
|
||||
if ( rec.m_comment.length() == 0 ) return false;
|
||||
|
||||
out->write( titleformat_inputtypes::meta, rec.m_comment.c_str() );
|
||||
|
||||
return true;
|
||||
} else {
|
||||
out->write(titleformat_inputtypes::meta, pfc::format_hex(hash,16) );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static service_factory_single_t<metadb_display_field_provider_impl> g_metadb_display_field_provider_impl;
|
||||
|
||||
static void cycleRating( const GUID & whichID, metadb_handle_list_cref tracks) {
|
||||
|
||||
const size_t count = tracks.get_count();
|
||||
if (count == 0) return;
|
||||
|
||||
auto client = clientByGUID(whichID);
|
||||
|
||||
rating_t rating = rating_invalid;
|
||||
|
||||
// Sorted/dedup'd set of all hashes of p_data items.
|
||||
// pfc::avltree_t<> is pfc equivalent of std::set<>.
|
||||
// We go the avltree_t<> route because more than one track in p_data might produce the same hash value, see metadb_index_client_impl / strPinTo
|
||||
pfc::avltree_t<metadb_index_hash> allHashes;
|
||||
for (size_t w = 0; w < count; ++w) {
|
||||
metadb_index_hash hash;
|
||||
if (client->hashHandle(tracks[w], hash)) {
|
||||
allHashes += hash;
|
||||
|
||||
// Take original rating to increment from the first selected song
|
||||
if (w == 0) rating = rating_get(whichID, hash);
|
||||
}
|
||||
}
|
||||
|
||||
if (allHashes.get_count() == 0) {
|
||||
FB2K_console_formatter() << "[foo_sample rating] Could not hash any of the tracks due to unavailable metadata, bailing";
|
||||
return;
|
||||
}
|
||||
|
||||
// Now cycle the rating value
|
||||
if (rating == rating_invalid) rating = 1;
|
||||
else if (rating >= rating_max) rating = rating_invalid;
|
||||
else ++rating;
|
||||
|
||||
// Now set the new rating
|
||||
pfc::list_t<metadb_index_hash> lstChanged; // Linear list of hashes that actually changed
|
||||
for (auto iter = allHashes.first(); iter.is_valid(); ++iter) {
|
||||
const metadb_index_hash hash = *iter;
|
||||
if (rating_set(whichID, hash, rating) ) { // rating_set returns true if the value actually changed, false if old equals new and no change was made
|
||||
lstChanged += hash;
|
||||
}
|
||||
}
|
||||
|
||||
FB2K_console_formatter() << "[foo_sample rating] " << lstChanged.get_count() << " entries updated";
|
||||
if (lstChanged.get_count() > 0) {
|
||||
// This gracefully tells everyone about what just changed, in one pass regardless of how many items got altered
|
||||
theAPI()->dispatch_refresh(whichID, lstChanged);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void cycleComment( const GUID & whichID, metadb_handle_list_cref tracks ) {
|
||||
const size_t count = tracks.get_count();
|
||||
if (count == 0) return;
|
||||
|
||||
auto client = clientByGUID(whichID);
|
||||
|
||||
pfc::string8 comment;
|
||||
|
||||
// Sorted/dedup'd set of all hashes of p_data items.
|
||||
// pfc::avltree_t<> is pfc equivalent of std::set<>.
|
||||
// We go the avltree_t<> route because more than one track in p_data might produce the same hash value, see metadb_index_client_impl / strPinTo
|
||||
pfc::avltree_t<metadb_index_hash> allHashes;
|
||||
for (size_t w = 0; w < count; ++w) {
|
||||
metadb_index_hash hash;
|
||||
if (client->hashHandle(tracks[w], hash)) {
|
||||
allHashes += hash;
|
||||
|
||||
// Take original rating to increment from the first selected song
|
||||
if (w == 0) comment = record_get(whichID, hash).m_comment;
|
||||
}
|
||||
}
|
||||
|
||||
if (allHashes.get_count() == 0) {
|
||||
FB2K_console_formatter() << "[foo_sample rating] Could not hash any of the tracks due to unavailable metadata, bailing";
|
||||
return;
|
||||
}
|
||||
|
||||
// Now cycle the comment value
|
||||
if ( comment.equals("") ) comment = "foo";
|
||||
else if ( comment.equals("foo") ) comment = "bar";
|
||||
else comment = "";
|
||||
|
||||
// Now apply the new comment
|
||||
pfc::list_t<metadb_index_hash> lstChanged; // Linear list of hashes that actually changed
|
||||
for (auto iter = allHashes.first(); iter.is_valid(); ++iter) {
|
||||
const metadb_index_hash hash = *iter;
|
||||
|
||||
if ( comment_set(whichID, hash, comment) ) {
|
||||
lstChanged += hash;
|
||||
}
|
||||
}
|
||||
|
||||
FB2K_console_formatter() << "[foo_sample rating] " << lstChanged.get_count() << " entries updated";
|
||||
if (lstChanged.get_count() > 0) {
|
||||
// This gracefully tells everyone about what just changed, in one pass regardless of how many items got altered
|
||||
theAPI()->dispatch_refresh(whichID, lstChanged);
|
||||
}
|
||||
}
|
||||
|
||||
class contextmenu_rating : public contextmenu_item_simple {
|
||||
public:
|
||||
GUID get_parent() {
|
||||
return guid_foo_sample_contextmenu_group;
|
||||
}
|
||||
unsigned get_num_items() {
|
||||
return 4;
|
||||
}
|
||||
void get_item_name(unsigned p_index, pfc::string_base & p_out) {
|
||||
PFC_ASSERT( p_index < get_num_items() );
|
||||
switch(p_index) {
|
||||
case 0:
|
||||
p_out = "Cycle track rating"; break;
|
||||
case 1:
|
||||
p_out = "Cycle album rating"; break;
|
||||
case 2:
|
||||
p_out = "Cycle track comment"; break;
|
||||
case 3:
|
||||
p_out = "Cycle album comment"; break;
|
||||
}
|
||||
|
||||
}
|
||||
void context_command(unsigned p_index, metadb_handle_list_cref p_data, const GUID& p_caller) {
|
||||
PFC_ASSERT( p_index < get_num_items() );
|
||||
|
||||
const GUID whichID = ((p_index%2) == 1) ? guid_foo_sample_album_rating_index : guid_foo_sample_track_rating_index;
|
||||
|
||||
if ( p_index < 2 ) {
|
||||
// rating
|
||||
cycleRating( whichID, p_data );
|
||||
} else {
|
||||
cycleComment( whichID, p_data );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
GUID get_item_guid(unsigned p_index) {
|
||||
switch(p_index) {
|
||||
case 0: return guid_foo_sample_rating_contextmenu1;
|
||||
case 1: return guid_foo_sample_rating_contextmenu2;
|
||||
case 2: return guid_foo_sample_rating_contextmenu3;
|
||||
case 3: return guid_foo_sample_rating_contextmenu4;
|
||||
default: uBugCheck();
|
||||
}
|
||||
}
|
||||
bool get_item_description(unsigned p_index, pfc::string_base & p_out) {
|
||||
PFC_ASSERT( p_index < get_num_items() );
|
||||
switch( p_index ) {
|
||||
case 0:
|
||||
p_out = "Alters foo_sample's track rating on one or more selected tracks. Use %foo_sample_track_rating% to display the rating.";
|
||||
return true;
|
||||
case 1:
|
||||
p_out = "Alters foo_sample's album rating on one or more selected tracks. Use %foo_sample_album_rating% to display the rating.";
|
||||
return true;
|
||||
case 2:
|
||||
p_out = "Alters foo_sample's track comment on one or more selected tracks. Use %foo_sample_track_comment% to display the comment.";
|
||||
return true;
|
||||
case 3:
|
||||
p_out = "Alters foo_sample's album comment on one or more selected tracks. Use %foo_sample_album_comment% to display the comment.";
|
||||
return true;
|
||||
default:
|
||||
PFC_ASSERT(!"Should not get here");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static contextmenu_item_factory_t< contextmenu_rating > g_contextmenu_rating;
|
||||
|
||||
static pfc::string_formatter formatRatingDump(const GUID & whichID) {
|
||||
auto api = theAPI();
|
||||
pfc::list_t<metadb_index_hash> hashes;
|
||||
api->get_all_hashes(whichID, hashes);
|
||||
pfc::string_formatter message;
|
||||
message << "The database contains " << hashes.get_count() << " hashes.\n";
|
||||
for( size_t hashWalk = 0; hashWalk < hashes.get_count(); ++ hashWalk ) {
|
||||
auto hash = hashes[hashWalk];
|
||||
message << pfc::format_hex( hash, 8 ) << " : ";
|
||||
auto rec = record_get(whichID, hash);
|
||||
if ( rec.m_rating == rating_invalid ) message << "no rating";
|
||||
else message << "rating " << rec.m_rating;
|
||||
if ( rec.m_comment.length() > 0 ) {
|
||||
message << ", comment: " << rec.m_comment;
|
||||
}
|
||||
|
||||
metadb_handle_list tracks;
|
||||
|
||||
// Note that this returns only handles present in the media library
|
||||
|
||||
// Extra work is required if the user has no media library but only playlists,
|
||||
// have to walk the playlists and match hashes by yourself instead of calling this method
|
||||
api->get_ML_handles(whichID, hash, tracks);
|
||||
|
||||
|
||||
if ( tracks.get_count() == 0 ) message << ", no matching tracks in Media Library\n";
|
||||
else {
|
||||
message << ", " << tracks.get_count() << " matching track(s)\n";
|
||||
for( size_t w = 0; w < tracks.get_count(); ++ w ) {
|
||||
// pfc string formatter operator<< for metadb_handle prints the location
|
||||
message << tracks[w] << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
class mainmenu_rating : public mainmenu_commands {
|
||||
public:
|
||||
t_uint32 get_command_count() {
|
||||
return 1;
|
||||
}
|
||||
GUID get_command(t_uint32 p_index) {
|
||||
return guid_foo_sample_rating_mainmenu;
|
||||
}
|
||||
void get_name(t_uint32 p_index, pfc::string_base & p_out) {
|
||||
PFC_ASSERT( p_index == 0 );
|
||||
p_out = "Dump rating database";
|
||||
}
|
||||
bool get_description(t_uint32 p_index, pfc::string_base & p_out) {
|
||||
PFC_ASSERT(p_index == 0);
|
||||
p_out = "Shows a dump of the foo_sample rating database."; return true;
|
||||
}
|
||||
GUID get_parent() {
|
||||
return guid_foo_sample_mainmenu_group;
|
||||
}
|
||||
void execute(t_uint32 p_index, service_ptr_t<service_base> p_callback) {
|
||||
PFC_ASSERT( p_index == 0 );
|
||||
|
||||
try {
|
||||
|
||||
pfc::string_formatter dump;
|
||||
dump << "==== TRACK RATING DUMP ====\n" << formatRatingDump( guid_foo_sample_track_rating_index ) << "\n\n";
|
||||
dump << "==== ALBUM RATING DUMP ====\n" << formatRatingDump( guid_foo_sample_album_rating_index ) << "\n\n";
|
||||
|
||||
popup_message::g_show(dump, "foo_sample rating dump");
|
||||
} catch(std::exception const & e) {
|
||||
// should not really get here
|
||||
popup_message::g_complain("Rating dump failed", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
static service_factory_single_t<mainmenu_rating> g_mainmenu_rating;
|
||||
|
||||
|
||||
// This class provides our information for the properties dialog
|
||||
class track_property_provider_impl : public track_property_provider_v2 {
|
||||
public:
|
||||
void workThisIndex(GUID const & whichID, const char * whichName, double priorityBase, metadb_handle_list_cref p_tracks, track_property_callback & p_out) {
|
||||
auto client = clientByGUID( whichID );
|
||||
pfc::avltree_t<metadb_index_hash> hashes;
|
||||
const size_t trackCount = p_tracks.get_count();
|
||||
for (size_t trackWalk = 0; trackWalk < trackCount; ++trackWalk) {
|
||||
metadb_index_hash hash;
|
||||
if (client->hashHandle(p_tracks[trackWalk], hash)) {
|
||||
hashes += hash;
|
||||
}
|
||||
}
|
||||
|
||||
pfc::string8 strAverage = "N/A", strMin = "N/A", strMax = "N/A";
|
||||
pfc::string8 strComment;
|
||||
|
||||
{
|
||||
size_t count = 0;
|
||||
rating_t minval = rating_invalid;
|
||||
rating_t maxval = rating_invalid;
|
||||
uint64_t accum = 0;
|
||||
bool bFirst = true;
|
||||
bool bVarComments = false;
|
||||
for (auto i = hashes.first(); i.is_valid(); ++i) {
|
||||
auto rec = record_get(whichID, *i);
|
||||
auto r = rec.m_rating;
|
||||
if (r != rating_invalid) {
|
||||
++count;
|
||||
accum += r;
|
||||
|
||||
if (minval == rating_invalid || minval > r) minval = r;
|
||||
if (maxval == rating_invalid || maxval < r) maxval = r;
|
||||
}
|
||||
|
||||
|
||||
if ( bFirst ) {
|
||||
strComment = rec.m_comment;
|
||||
} else if ( ! bVarComments ) {
|
||||
if ( strComment != rec.m_comment ) {
|
||||
bVarComments = true;
|
||||
strComment = "<various>";
|
||||
}
|
||||
}
|
||||
|
||||
bFirst = false;
|
||||
}
|
||||
|
||||
|
||||
if (count > 0) {
|
||||
strMin = pfc::format_uint(minval);
|
||||
strMax = pfc::format_uint(maxval);
|
||||
strAverage = pfc::format_float((double)accum / (double)count, 0, 3);
|
||||
}
|
||||
}
|
||||
|
||||
p_out.set_property(strPropertiesGroup, priorityBase + 0, PFC_string_formatter() << "Average " << whichName << " Rating", strAverage);
|
||||
p_out.set_property(strPropertiesGroup, priorityBase + 1, PFC_string_formatter() << "Minimum " << whichName << " Rating", strMin);
|
||||
p_out.set_property(strPropertiesGroup, priorityBase + 2, PFC_string_formatter() << "Maximum " << whichName << " Rating", strMax);
|
||||
if ( strComment.length() > 0 ) {
|
||||
p_out.set_property(strPropertiesGroup, priorityBase + 3, PFC_string_formatter() << whichName << " Comment", strComment);
|
||||
}
|
||||
}
|
||||
void enumerate_properties(metadb_handle_list_cref p_tracks, track_property_callback & p_out) {
|
||||
workThisIndex( guid_foo_sample_track_rating_index, "Track", 0, p_tracks, p_out );
|
||||
workThisIndex( guid_foo_sample_album_rating_index, "Album", 10, p_tracks, p_out);
|
||||
}
|
||||
void enumerate_properties_v2(metadb_handle_list_cref p_tracks, track_property_callback_v2 & p_out) {
|
||||
if ( p_out.is_group_wanted( strPropertiesGroup ) ) {
|
||||
enumerate_properties( p_tracks, p_out );
|
||||
}
|
||||
}
|
||||
|
||||
bool is_our_tech_info(const char * p_name) {
|
||||
// If we do stuff with tech infos read from the file itself (see file_info::info_* methods), signal whether this field belongs to us
|
||||
// We don't do any of this, hence false
|
||||
return false;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
static service_factory_single_t<track_property_provider_impl> g_track_property_provider_impl;
|
||||
}
|
||||
32
foobar2000/foo_sample/readme.txt
Normal file
32
foobar2000/foo_sample/readme.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
This component demonstrates:
|
||||
* main.cpp :
|
||||
* Declaring your component's version information.
|
||||
* input_raw.cpp :
|
||||
* Declaring your own "input" classes for decoding additional audio file formats.
|
||||
* Calling file system services.
|
||||
* preferences.cpp :
|
||||
* Declaring your configuration variables.
|
||||
* Creating preferences pages using simple WTL dialogs.
|
||||
* Declaring advanced preferences entries.
|
||||
* initquit.cpp :
|
||||
* Sample initialization/shutdown callback service.
|
||||
* dsp.cpp :
|
||||
* Sample DSP.
|
||||
* contextmenu.cpp :
|
||||
* Sample context menu command.
|
||||
* decode.cpp :
|
||||
* Getting PCM data from arbitrary audio files.
|
||||
* Use of the threaded_process API to easily run time-consuming tasks in worker threads with progress dialogs.
|
||||
* mainmenu.cpp :
|
||||
* Sample main menu command
|
||||
* playback_state.cpp :
|
||||
* Use of playback callbacks.
|
||||
* Use of playback control.
|
||||
* ui_element.cpp :
|
||||
* Simple UI Element implementation.
|
||||
* rating.cpp
|
||||
* Minimal rating+comment implementation using metadb_index_client.
|
||||
* Present your data via title formatting using metadb_display_field_provider.
|
||||
* Present your data in the properties dialog using track_property_provider.
|
||||
* Utility menu items.
|
||||
* Basic use of stream formatters.
|
||||
46
foobar2000/foo_sample/resource.h
Normal file
46
foobar2000/foo_sample/resource.h
Normal file
@@ -0,0 +1,46 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by foo_sample.rc
|
||||
//
|
||||
#define IDD_PLAYBACK_STATE 101
|
||||
#define IDD_DSP 102
|
||||
#define IDD_UI_ELEMENT 103
|
||||
#define IDD_THREADS 105
|
||||
#define IDD_LISTCONTROL_DEMO 107
|
||||
#define IDI_ICON1 109
|
||||
#define IDI_SCROLL 109
|
||||
#define IDD_MYPREFERENCES 148
|
||||
#define IDC_BOGO1 1001
|
||||
#define IDC_BOGO2 1002
|
||||
#define IDC_PATTERN 1002
|
||||
#define IDC_STATE 1003
|
||||
#define IDC_PLAY 1004
|
||||
#define IDC_PAUSE 1005
|
||||
#define IDC_STOP 1006
|
||||
#define IDC_PREV 1007
|
||||
#define IDC_NEXT 1008
|
||||
#define IDC_RAND 1009
|
||||
#define IDC_CONTEXTMENU 1010
|
||||
#define IDC_SLIDER1 1012
|
||||
#define IDC_SLIDER 1012
|
||||
#define IDC_SLIDER_LABEL 1013
|
||||
#define IDC_LOCK_MIN_WIDTH 1014
|
||||
#define IDC_LOCK_MIN_HEIGHT 1015
|
||||
#define IDC_CHECK3 1016
|
||||
#define IDC_LOCK_MAX_WIDTH 1016
|
||||
#define IDC_LOCK_MAX_HEIGHT 1017
|
||||
#define IDC_STATIC_SIZE 1018
|
||||
#define IDC_LIST1 1019
|
||||
#define IDC_LIST 1019
|
||||
#define IDC_HEADER 1020
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 110
|
||||
#define _APS_NEXT_COMMAND_VALUE 40001
|
||||
#define _APS_NEXT_CONTROL_VALUE 1021
|
||||
#define _APS_NEXT_SYMED_VALUE 101
|
||||
#endif
|
||||
#endif
|
||||
2
foobar2000/foo_sample/stdafx.h
Normal file
2
foobar2000/foo_sample/stdafx.h
Normal file
@@ -0,0 +1,2 @@
|
||||
#include <helpers/foobar2000+atl.h>
|
||||
|
||||
286
foobar2000/foo_sample/ui_and_threads.cpp
Normal file
286
foobar2000/foo_sample/ui_and_threads.cpp
Normal file
@@ -0,0 +1,286 @@
|
||||
#include "stdafx.h"
|
||||
#include "resource.h"
|
||||
|
||||
// Modern multi threading with C++
|
||||
// Or: how I learned to stop worrying and love the lambdas
|
||||
|
||||
#include <memory> // shared_ptr
|
||||
#include <libPPUI/CDialogResizeHelper.h>
|
||||
#include <helpers/filetimetools.h>
|
||||
#include <helpers/duration_counter.h>
|
||||
#include <helpers/atl-misc.h>
|
||||
|
||||
|
||||
|
||||
namespace { // anon namespace local classes for good measure
|
||||
|
||||
|
||||
class CDemoDialog; // forward declaration
|
||||
|
||||
// This is kept a separate shared_ptr'd struct because it may outlive CDemoDialog instance.
|
||||
struct sharedData_t {
|
||||
metadb_handle_list items;
|
||||
CDemoDialog * owner; // weak reference to the owning dialog; can be only used after checking the validity by other means.
|
||||
};
|
||||
|
||||
static const CDialogResizeHelper::Param resizeData[] = {
|
||||
// Dialog resize handling matrix, defines how the controls scale with the dialog
|
||||
// L T R B
|
||||
{IDOK, 1,1,1,1 },
|
||||
{IDCANCEL, 1,1,1,1 },
|
||||
{IDC_HEADER, 0,0,1,0 },
|
||||
{IDC_LIST, 0,0,1,1 },
|
||||
|
||||
// current position of a control is determined by initial_position + factor * (current_dialog_size - initial_dialog_size)
|
||||
// where factor is the value from the table above
|
||||
// applied to all four values - left, top, right, bottom
|
||||
// 0,0,0,0 means that a control doesn't react to dialog resizing (aligned to top+left, no resize)
|
||||
// 1,1,1,1 means that the control is aligned to bottom+right but doesn't resize
|
||||
// 0,0,1,0 means that the control disregards vertical resize (aligned to top) and changes its width with the dialog
|
||||
};
|
||||
|
||||
// Minimum/maximum size, in dialog box units; see MSDN MapDialogRect for more info about dialog box units.
|
||||
// The values can be declared constant here and will be scaled appropriately depending on display DPI.
|
||||
static const CRect resizeMinMax(150, 100, 1000, 1000);
|
||||
|
||||
class CDemoDialog : public CDialogImpl<CDemoDialog> {
|
||||
public:
|
||||
enum { IDD = IDD_THREADS };
|
||||
CDemoDialog( metadb_handle_list_cref items ) : m_resizer(resizeData, resizeMinMax) {
|
||||
m_sharedData = std::make_shared< sharedData_t > ();
|
||||
m_sharedData->items = items;
|
||||
m_sharedData->owner = this;
|
||||
}
|
||||
|
||||
BEGIN_MSG_MAP_EX(CDemoDialog)
|
||||
CHAIN_MSG_MAP_MEMBER(m_resizer)
|
||||
MSG_WM_INITDIALOG(OnInitDialog)
|
||||
COMMAND_HANDLER_EX(IDOK, BN_CLICKED, OnOK)
|
||||
COMMAND_HANDLER_EX(IDCANCEL, BN_CLICKED, OnCancel)
|
||||
MSG_WM_CLOSE(OnClose)
|
||||
MSG_WM_DESTROY(OnDestroy)
|
||||
MSG_WM_SIZE(OnSize)
|
||||
END_MSG_MAP()
|
||||
private:
|
||||
BOOL OnInitDialog(CWindow, LPARAM) {
|
||||
uSetDlgItemText(*this, IDC_HEADER, PFC_string_formatter() << "Selected: " << m_sharedData->items.get_size() << " tracks." );
|
||||
m_listBox = GetDlgItem(IDC_LIST);
|
||||
|
||||
m_statusBar.Create(*this, NULL, TEXT(""), WS_CHILD | WS_VISIBLE);
|
||||
|
||||
m_statusBar.SetWindowText(L"Ready");
|
||||
|
||||
ShowWindow(SW_SHOW);
|
||||
|
||||
return TRUE; // system should set focus
|
||||
}
|
||||
void OnSize(UINT nType, CSize size) {
|
||||
// Tell statusbar that we got resized. CDialogResizeHelper can't do this for us.
|
||||
m_statusBar.SendMessage(WM_SIZE);
|
||||
}
|
||||
void OnDestroy() {
|
||||
cancelTask();
|
||||
}
|
||||
|
||||
void OnClose() {
|
||||
// NOTE if we do not handle WM_CLOSE, WM_COMMAND with IDCANCEL will be invoked, executing our cancel handler.
|
||||
// We provide our own WM_CLOSE handler to provide a different response to closing the window.
|
||||
DestroyWindow();
|
||||
}
|
||||
|
||||
void OnCancel(UINT, int, CWindow) {
|
||||
// If a task is active, cancel it
|
||||
// otherwise destroy the dialog
|
||||
if (! cancelTask() ) {
|
||||
DestroyWindow();
|
||||
} else {
|
||||
// Refresh UI
|
||||
taskCompleted();
|
||||
}
|
||||
}
|
||||
void OnOK(UINT, int, CWindow) {
|
||||
startTask();
|
||||
}
|
||||
void startTask() {
|
||||
cancelTask(); // cancel any running task
|
||||
|
||||
GetDlgItem(IDCANCEL).SetWindowText(L"Cancel");
|
||||
m_statusBar.SetWindowText(L"Working...");
|
||||
|
||||
auto shared = m_sharedData;
|
||||
auto aborter = std::make_shared<abort_callback_impl>();
|
||||
m_aborter = aborter;
|
||||
|
||||
// New in fb2k 1.4.5: async_task_manager & splitTask
|
||||
// Use fb2k::splitTask() for starting detached threads.
|
||||
// In fb2k < 1.4.5, it will fall back to just starting a detached thread.
|
||||
// fb2k 1.4.5+ async_task_manager cleanly deals with user exiting foobar2000 while a detached async task is running.
|
||||
// Shutdown of foobar2000 process will be stalled until your task completes.
|
||||
// If you use other means to ensure that the thread has finished, such as waiting for the thread to exit in your dialog's destructor, there's no need for this.
|
||||
|
||||
fb2k::splitTask( [aborter, shared] {
|
||||
// In worker thread!
|
||||
try {
|
||||
work( shared, aborter );
|
||||
} catch(exception_aborted) {
|
||||
return; // user abort?
|
||||
} catch(std::exception const & e) {
|
||||
// should not really get here
|
||||
logLineProc( shared, aborter, PFC_string_formatter() << "Critical error: " << e);
|
||||
}
|
||||
try {
|
||||
mainThreadOp( aborter, [shared] {
|
||||
shared->owner->taskCompleted();
|
||||
} );
|
||||
} catch(...) {} // mainThreadOp may throw exception_aborted
|
||||
} );
|
||||
}
|
||||
void taskCompleted() {
|
||||
m_aborter.reset();
|
||||
GetDlgItem(IDCANCEL).SetWindowText(L"Close");
|
||||
m_statusBar.SetWindowText(L"Finished, ready");
|
||||
}
|
||||
|
||||
static void mainThreadOp(std::shared_ptr<abort_callback_impl> aborter, std::function<void ()> op ) {
|
||||
aborter->check(); // are we getting aborted?
|
||||
fb2k::inMainThread( [=] {
|
||||
if ( aborter->is_set() ) return; // final user abort check
|
||||
// Past this, we're main thread, the task has not been cancelled by the user and the dialog is still alive
|
||||
// and any dialog methods can be safely called
|
||||
op();
|
||||
} );
|
||||
}
|
||||
|
||||
static void logLineProc(std::shared_ptr<sharedData_t> shared, std::shared_ptr<abort_callback_impl> aborter, const char * line_ ) {
|
||||
pfc::string8 line( line_ ); // can't hold to the const char* we got passed, we have no guarantees about its lifetime
|
||||
mainThreadOp( aborter, [shared, line] {
|
||||
shared->owner->logLine(line);
|
||||
} );
|
||||
}
|
||||
static void work( std::shared_ptr<sharedData_t> shared, std::shared_ptr<abort_callback_impl> aborter ) {
|
||||
// clear the log
|
||||
mainThreadOp(aborter, [shared] {
|
||||
shared->owner->clearLog();
|
||||
} );
|
||||
|
||||
// A convenience wrapper that calls logLineProc()
|
||||
auto log = [shared, aborter] ( const char * line ) {
|
||||
logLineProc(shared, aborter, line);
|
||||
};
|
||||
// Use log(X) instead of logLineProc(shared, aborter, X)
|
||||
|
||||
for( size_t trackWalk = 0; trackWalk < shared->items.get_size(); ++ trackWalk ) {
|
||||
aborter->check();
|
||||
auto track = shared->items[trackWalk];
|
||||
log( PFC_string_formatter() << "Track: " << track );
|
||||
|
||||
try {
|
||||
const auto path = track->get_path();
|
||||
const auto subsong = track->get_subsong_index();
|
||||
|
||||
// Not strictly needed, but we do it anyway
|
||||
// Acquire a read lock on the file, so anyone trying to acquire a write lock will just wait till we have finished
|
||||
auto lock = file_lock_manager::get()->acquire_read(path, *aborter);
|
||||
|
||||
{
|
||||
input_decoder::ptr dec;
|
||||
input_entry::g_open_for_decoding(dec, nullptr, path, *aborter);
|
||||
|
||||
file_info_impl info;
|
||||
dec->get_info( subsong, info, *aborter );
|
||||
auto title = info.meta_get("title",0);
|
||||
if ( title == nullptr ) log("Untitled");
|
||||
else log(PFC_string_formatter() << "Title: " << title );
|
||||
if ( info.get_length() > 0 ) log(PFC_string_formatter() << "Duration: " << pfc::format_time_ex(info.get_length(),6) );
|
||||
auto stats = dec->get_file_stats( *aborter );
|
||||
if ( stats.m_size != filesize_invalid ) log( PFC_string_formatter() << "Size: " << pfc::format_file_size_short(stats.m_size) );
|
||||
if ( stats.m_timestamp != filetimestamp_invalid ) log( PFC_string_formatter() << "Last modified: " << format_filetimestamp( stats.m_timestamp ) );
|
||||
|
||||
|
||||
dec->initialize( subsong, input_flag_simpledecode, * aborter );
|
||||
audio_chunk_impl chunk;
|
||||
uint64_t numChunks = 0, numSamples = 0;
|
||||
// duration_counter tool is a strictly accurate audio duration counter retaining all sample counts passed to it, immune to floatingpoint accuracy errors
|
||||
duration_counter duration;
|
||||
bool firstChunk = true;
|
||||
while(dec->run(chunk, *aborter) ) {
|
||||
if ( firstChunk ) {
|
||||
auto spec = chunk.get_spec();
|
||||
log(PFC_string_formatter() << "Audio sample rate: " << spec.sampleRate );
|
||||
log(PFC_string_formatter() << "Audio channels: " << audio_chunk::g_formatChannelMaskDesc( spec.chanMask ) );
|
||||
firstChunk = false;
|
||||
}
|
||||
++ numChunks;
|
||||
duration += chunk;
|
||||
numSamples += chunk.get_sample_count();
|
||||
}
|
||||
log(PFC_string_formatter() << "Decoded " << numChunks << " chunks");
|
||||
log(PFC_string_formatter() << "Exact duration decoded: " << pfc::format_time_ex(duration.query(), 6) << ", " << numSamples << " samples" );
|
||||
}
|
||||
|
||||
try {
|
||||
auto aa = album_art_extractor::g_open( nullptr, path, *aborter );
|
||||
|
||||
if ( aa->have_entry( album_art_ids::cover_front, *aborter ) ) {
|
||||
log("Album art: front cover found");
|
||||
}
|
||||
if ( aa->have_entry( album_art_ids::cover_back, *aborter ) ) {
|
||||
log("Album art: back cover found");
|
||||
}
|
||||
if (aa->have_entry( album_art_ids::artist, *aborter ) ) {
|
||||
log("Album art: artist picture found");
|
||||
}
|
||||
} catch(exception_album_art_not_found) {
|
||||
} catch(exception_album_art_unsupported_format) {
|
||||
}
|
||||
|
||||
} catch(exception_aborted) {
|
||||
throw;
|
||||
} catch(std::exception const & e) {
|
||||
log( PFC_string_formatter() << "Failure: " << e);
|
||||
}
|
||||
}
|
||||
log("All done.");
|
||||
}
|
||||
|
||||
bool cancelTask() {
|
||||
bool ret = false;
|
||||
auto aborter = pfc::replace_null_t(m_aborter);
|
||||
if (aborter) {
|
||||
ret = true;
|
||||
aborter->set();
|
||||
logLine("Aborted by user.");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void logLine( const char * line ) {
|
||||
m_listBox.AddString( pfc::stringcvt::string_os_from_utf8(line) );
|
||||
}
|
||||
|
||||
void clearLog() {
|
||||
m_listBox.ResetContent();
|
||||
}
|
||||
|
||||
|
||||
// Worker thread aborter. It's re-created with the thread. If the task is ran more than once, each time it gets a new one.
|
||||
// A commonly used alternative is to have abort_callback_impl m_aborter, and a blocking cancelTask() that waits for the thread to exit, without all the shared_ptrs and recreation of aborters.
|
||||
// However that approach will freeze the UI if the worker thread is taking a long time to exit, as well as require some other shared_ptr based means for fb2k::inMainThread() ops to verify that the task is not being aborted / the dialog still exists.
|
||||
// Therefore we use a shared_ptr'd aborter, which is used both to abort worker threads, and for main thread callbacks to know if the task that sent them is still valid.
|
||||
std::shared_ptr<abort_callback_impl> m_aborter;
|
||||
|
||||
// Data shared with the worker thread. It is created only once per dialog lifetime.
|
||||
std::shared_ptr< sharedData_t > m_sharedData;
|
||||
|
||||
CListBox m_listBox;
|
||||
|
||||
CStatusBarCtrl m_statusBar;
|
||||
|
||||
CDialogResizeHelper m_resizer;
|
||||
};
|
||||
}
|
||||
|
||||
void RunUIAndThreads(metadb_handle_list_cref data) {
|
||||
// Equivalent to new CDemoDialog(data), with modeless registration and auto lifetime
|
||||
fb2k::newDialog<CDemoDialog>( data );
|
||||
}
|
||||
82
foobar2000/foo_sample/ui_element.cpp
Normal file
82
foobar2000/foo_sample/ui_element.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#include "stdafx.h"
|
||||
|
||||
#include <libPPUI/win32_op.h>
|
||||
#include <helpers/BumpableElem.h>
|
||||
|
||||
|
||||
namespace {
|
||||
// Anonymous namespace : standard practice in fb2k components
|
||||
// Nothing outside should have any reason to see these symbols, and we don't want funny results if another cpp has similarly named classes.
|
||||
// service_factory at the bottom takes care of publishing our class.
|
||||
|
||||
// This is our GUID. Substitute with your own when reusing code.
|
||||
static const GUID guid_myelem = { 0xb46dc166, 0x88f3, 0x4b45, { 0x9f, 0x77, 0xab, 0x33, 0xf4, 0xc3, 0xf2, 0xe4 } };
|
||||
|
||||
class CMyElemWindow : public ui_element_instance, public CWindowImpl<CMyElemWindow> {
|
||||
public:
|
||||
// ATL window class declaration. Replace class name with your own when reusing code.
|
||||
DECLARE_WND_CLASS_EX(TEXT("{DC2917D5-1288-4434-A28C-F16CFCE13C4B}"),CS_VREDRAW | CS_HREDRAW,(-1));
|
||||
|
||||
void initialize_window(HWND parent) {WIN32_OP(Create(parent) != NULL);}
|
||||
|
||||
BEGIN_MSG_MAP_EX(CMyElemWindow)
|
||||
MESSAGE_HANDLER(WM_LBUTTONDOWN,OnLButtonDown);
|
||||
MSG_WM_ERASEBKGND(OnEraseBkgnd)
|
||||
MSG_WM_PAINT(OnPaint)
|
||||
END_MSG_MAP()
|
||||
|
||||
CMyElemWindow(ui_element_config::ptr,ui_element_instance_callback_ptr p_callback);
|
||||
HWND get_wnd() {return *this;}
|
||||
void set_configuration(ui_element_config::ptr config) {m_config = config;}
|
||||
ui_element_config::ptr get_configuration() {return m_config;}
|
||||
static GUID g_get_guid() {
|
||||
return guid_myelem;
|
||||
}
|
||||
static GUID g_get_subclass() {return ui_element_subclass_utility;}
|
||||
static void g_get_name(pfc::string_base & out) {out = "Sample UI Element";}
|
||||
static ui_element_config::ptr g_get_default_configuration() {return ui_element_config::g_create_empty(g_get_guid());}
|
||||
static const char * g_get_description() {return "This is a sample UI Element.";}
|
||||
|
||||
void notify(const GUID & p_what, t_size p_param1, const void * p_param2, t_size p_param2size);
|
||||
private:
|
||||
LRESULT OnLButtonDown(UINT,WPARAM,LPARAM,BOOL&) {m_callback->request_replace(this);return 0;}
|
||||
void OnPaint(CDCHandle);
|
||||
BOOL OnEraseBkgnd(CDCHandle);
|
||||
ui_element_config::ptr m_config;
|
||||
protected:
|
||||
// this must be declared as protected for ui_element_impl_withpopup<> to work.
|
||||
const ui_element_instance_callback_ptr m_callback;
|
||||
};
|
||||
void CMyElemWindow::notify(const GUID & p_what, t_size p_param1, const void * p_param2, t_size p_param2size) {
|
||||
if (p_what == ui_element_notify_colors_changed || p_what == ui_element_notify_font_changed) {
|
||||
// we use global colors and fonts - trigger a repaint whenever these change.
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
CMyElemWindow::CMyElemWindow(ui_element_config::ptr config,ui_element_instance_callback_ptr p_callback) : m_callback(p_callback), m_config(config) {
|
||||
}
|
||||
|
||||
BOOL CMyElemWindow::OnEraseBkgnd(CDCHandle dc) {
|
||||
CRect rc; WIN32_OP_D( GetClientRect(&rc) );
|
||||
CBrush brush;
|
||||
WIN32_OP_D( brush.CreateSolidBrush( m_callback->query_std_color(ui_color_background) ) != NULL );
|
||||
WIN32_OP_D( dc.FillRect(&rc, brush) );
|
||||
return TRUE;
|
||||
}
|
||||
void CMyElemWindow::OnPaint(CDCHandle) {
|
||||
CPaintDC dc(*this);
|
||||
dc.SetTextColor( m_callback->query_std_color(ui_color_text) );
|
||||
dc.SetBkMode(TRANSPARENT);
|
||||
SelectObjectScope fontScope(dc, (HGDIOBJ) m_callback->query_font_ex(ui_font_default) );
|
||||
const UINT format = DT_NOPREFIX | DT_CENTER | DT_VCENTER | DT_SINGLELINE;
|
||||
CRect rc;
|
||||
WIN32_OP_D( GetClientRect(&rc) );
|
||||
WIN32_OP_D( dc.DrawText(_T("This is a sample element."), -1, &rc, format) > 0 );
|
||||
}
|
||||
|
||||
// ui_element_impl_withpopup autogenerates standalone version of our component and proper menu commands. Use ui_element_impl instead if you don't want that.
|
||||
class ui_element_myimpl : public ui_element_impl_withpopup<CMyElemWindow> {};
|
||||
|
||||
static service_factory_single_t<ui_element_myimpl> g_ui_element_myimpl_factory;
|
||||
|
||||
} // namespace
|
||||
178
foobar2000/foo_sample/ui_element_dialog.cpp
Normal file
178
foobar2000/foo_sample/ui_element_dialog.cpp
Normal file
@@ -0,0 +1,178 @@
|
||||
#include "stdafx.h"
|
||||
|
||||
#include "resource.h"
|
||||
|
||||
#include <libPPUI/win32_utility.h>
|
||||
#include <libPPUI/win32_op.h> // WIN32_OP()
|
||||
#include <libPPUI/wtl-pp.h> // CCheckBox
|
||||
#include <helpers/atl-misc.h> // ui_element_impl
|
||||
|
||||
namespace {
|
||||
// Anonymous namespace : standard practice in fb2k components
|
||||
// Nothing outside should have any reason to see these symbols, and we don't want funny results if another cpp has similarly named classes.
|
||||
// service_factory at the bottom takes care of publishing our class.
|
||||
|
||||
|
||||
// I am Sample Component and this is *MY* GUID.
|
||||
// Replace with your own when reusing code. Component authors with colliding GUIDs will be visited by Urdnot Wrex in person.
|
||||
static const GUID guid_myelem = { 0x78ca1d7, 0x4e3a, 0x41d5, { 0xa5, 0xef, 0x9d, 0x1a, 0xf7, 0xd5, 0x79, 0xd0 } };
|
||||
|
||||
enum {
|
||||
FlagLockMinWidth = 1 << 0,
|
||||
FlagLockMinHeight = 1 << 1,
|
||||
FlagLockMaxWidth = 1 << 2,
|
||||
FlagLockMaxHeight = 1 << 3,
|
||||
|
||||
FlagsDefault = 0
|
||||
};
|
||||
static const struct {
|
||||
int btnID;
|
||||
uint32_t flag;
|
||||
} flagsAndButtons[] = {
|
||||
{ IDC_LOCK_MIN_WIDTH, FlagLockMinWidth },
|
||||
{ IDC_LOCK_MIN_HEIGHT, FlagLockMinHeight },
|
||||
{ IDC_LOCK_MAX_WIDTH, FlagLockMaxWidth },
|
||||
{ IDC_LOCK_MAX_HEIGHT, FlagLockMaxHeight },
|
||||
};
|
||||
|
||||
class CDialogUIElem : public CDialogImpl<CDialogUIElem>, public ui_element_instance {
|
||||
public:
|
||||
CDialogUIElem( ui_element_config::ptr cfg, ui_element_instance_callback::ptr cb ) : m_callback(cb), m_flags( parseConfig(cfg) ) {}
|
||||
|
||||
enum { IDD = IDD_UI_ELEMENT };
|
||||
|
||||
BEGIN_MSG_MAP_EX( CDialogUIElem )
|
||||
MSG_WM_INITDIALOG(OnInitDialog)
|
||||
MSG_WM_SIZE(OnSize)
|
||||
COMMAND_CODE_HANDLER_EX(BN_CLICKED, OnButtonClicked)
|
||||
END_MSG_MAP()
|
||||
|
||||
void initialize_window(HWND parent) {WIN32_OP(Create(parent) != NULL);}
|
||||
HWND get_wnd() { return m_hWnd; }
|
||||
void set_configuration(ui_element_config::ptr config) {
|
||||
m_flags = parseConfig( config );
|
||||
if ( m_hWnd != NULL ) {
|
||||
configToUI();
|
||||
}
|
||||
m_callback->on_min_max_info_change();
|
||||
}
|
||||
ui_element_config::ptr get_configuration() {return makeConfig(m_flags);}
|
||||
static GUID g_get_guid() {
|
||||
return guid_myelem;
|
||||
}
|
||||
static void g_get_name(pfc::string_base & out) {out = "Sample Dialog as UI Element";}
|
||||
static ui_element_config::ptr g_get_default_configuration() {
|
||||
return makeConfig( );
|
||||
}
|
||||
static const char * g_get_description() {return "This is a sample UI Element using win32 dialog.";}
|
||||
static GUID g_get_subclass() {return ui_element_subclass_utility;}
|
||||
|
||||
ui_element_min_max_info get_min_max_info() {
|
||||
ui_element_min_max_info ret;
|
||||
|
||||
// Note that we play nicely with separate horizontal & vertical DPI.
|
||||
// Such configurations have not been ever seen in circulation, but nothing stops us from supporting such.
|
||||
CSize DPI = QueryScreenDPIEx( *this );
|
||||
|
||||
if ( DPI.cx <= 0 || DPI.cy <= 0 ) { // sanity
|
||||
DPI = CSize(96, 96);
|
||||
}
|
||||
|
||||
if ( m_flags & FlagLockMinWidth ) {
|
||||
ret.m_min_width = MulDiv( 200, DPI.cx, 96 );
|
||||
}
|
||||
if ( m_flags & FlagLockMinHeight ) {
|
||||
ret.m_min_height = MulDiv( 200, DPI.cy, 96 );
|
||||
}
|
||||
if ( m_flags & FlagLockMaxWidth ) {
|
||||
ret.m_max_width = MulDiv( 400, DPI.cx, 96 );
|
||||
}
|
||||
if ( m_flags & FlagLockMaxHeight ) {
|
||||
ret.m_max_height = MulDiv( 400, DPI.cy, 96 );
|
||||
}
|
||||
|
||||
// Deal with WS_EX_STATICEDGE and alike that we might have picked from host
|
||||
ret.adjustForWindow( *this );
|
||||
|
||||
return ret;
|
||||
}
|
||||
private:
|
||||
static uint32_t parseConfig( ui_element_config::ptr cfg ) {
|
||||
try {
|
||||
::ui_element_config_parser in ( cfg );
|
||||
uint32_t flags; in >> flags;
|
||||
return flags;
|
||||
} catch(exception_io_data) {
|
||||
// If we got here, someone's feeding us nonsense, fall back to defaults
|
||||
return FlagsDefault;
|
||||
}
|
||||
}
|
||||
static ui_element_config::ptr makeConfig(uint32_t flags = FlagsDefault) {
|
||||
ui_element_config_builder out;
|
||||
out << flags;
|
||||
return out.finish( g_get_guid() );
|
||||
}
|
||||
void configToUI() {
|
||||
for ( unsigned i = 0; i < PFC_TABSIZE( flagsAndButtons ); ++ i ) {
|
||||
auto rec = flagsAndButtons[i];
|
||||
// CCheckBox: WTL-PP class overlaying ToggleCheck(bool) and bool IsChecked() over WTL CButton
|
||||
CCheckBox cb ( GetDlgItem( rec.btnID ) );
|
||||
cb.ToggleCheck( (m_flags & rec.flag ) != 0 );
|
||||
}
|
||||
}
|
||||
void OnButtonClicked(UINT uNotifyCode, int nID, CWindow wndCtl) {
|
||||
|
||||
uint32_t flagToFlip = 0;
|
||||
for ( unsigned i = 0; i < PFC_TABSIZE( flagsAndButtons ); ++ i ) {
|
||||
auto rec = flagsAndButtons[i];
|
||||
if ( rec.btnID == nID ) {
|
||||
flagToFlip = rec.flag;
|
||||
}
|
||||
}
|
||||
if ( flagToFlip != 0 ) {
|
||||
uint32_t newFlags = m_flags;
|
||||
CCheckBox cb ( wndCtl );
|
||||
if (cb.IsChecked()) {
|
||||
newFlags |= flagToFlip;
|
||||
} else {
|
||||
newFlags &= ~flagToFlip;
|
||||
}
|
||||
if ( newFlags != m_flags ) {
|
||||
m_flags = newFlags;
|
||||
m_callback->on_min_max_info_change();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnSize(UINT, CSize s) {
|
||||
auto DPI = QueryScreenDPIEx(*this);
|
||||
|
||||
pfc::string_formatter msg;
|
||||
msg << "Current size: ";
|
||||
if ( DPI.cx > 0 && DPI.cy > 0 ) {
|
||||
msg << MulDiv( s.cx, 96, DPI.cx ) << "x" << MulDiv( s.cy, 96, DPI.cy ) << " units, ";
|
||||
}
|
||||
msg << s.cx << "x" << s.cy << " pixels";
|
||||
|
||||
uSetDlgItemText( *this, IDC_STATIC_SIZE, msg );
|
||||
}
|
||||
BOOL OnInitDialog(CWindow, LPARAM) {
|
||||
configToUI();
|
||||
{
|
||||
CRect rc;
|
||||
// WIN32_OP_D() - Debug build only retval check and assert
|
||||
// For stuff that practically never fails
|
||||
WIN32_OP_D( GetClientRect( &rc ) );
|
||||
OnSize( 0, rc.Size() );
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
const ui_element_instance_callback::ptr m_callback;
|
||||
uint32_t m_flags;
|
||||
};
|
||||
|
||||
|
||||
static service_factory_single_t< ui_element_impl< CDialogUIElem > > g_CDialogUIElem_factory;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user