Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 4 years ago.
Improve this question
I would like to build a program which lets lyrics of a song run over the screen. Something like this:
http://www.youtube.com/watch?v=kIAiBvD9njM
Can you help me?
Algorithm:
pushes the marker to the right of a line fitting the music
lets a line above the current line disappear
inserts a new line above the current line
What is needed?
lyrics of the song (line per line)
time to text data? (when does a line start/end)
Some approaches would help me a lot. Pseudo-code or even Delphi code of any part would be fantastic.
If you're interested in karaoke code in pascal, make sure to take a look at UltraStar Deluxe.
It's a super slick and very popular karaoke application. The project is active and it's open source. It can be compiled to various platforms with FPC. You can compile it from both Delphi and Lazarus.. nice.
http://ultrastardx.sourceforge.net/
My neighbours thought that my dog was their worst nightmare until I found this program.
See it in action: po-po-po-pokerface po-po-pokerface.. mum mum mum mah! :)
let's assume you have a text file with the text to be shown and the annotated time of when to hightlight it (kind of a subtitles file, for example the standard proposal w3c timed text (http://www.w3.org/AudioVideo/TT/) or the SUB - Movie subtitle file format in use by several media players.
Your program must first read and parse the text file, and decode the annotated time. Insert it in a stringlist called Subtitles which items would also keep objects similar to this one
type tSubtitle = class
num : integer;
prevTime, fromTime : tdatetime;
toTime, nextTime: tdatetime;
text: string;
end;
You might want to extend the object to hold some highlighting attributes as well.
Then you just need to display those objects synchronized with a timer.
procedure TForm1.Timer1Timer(Sender: TObject);
var rt : TDateTime;
done:boolean;
si,st,sb:integer;
s:string;
begin
rt:=now-startTime;
st:=0;
sb:=subtitles.Count; // binary search the subtitle for the current time
repeat
si:=(st+sb) div 2;
s:=TSubtitle(subtitles.Objects[si-1]);
done:= ((t>=s.prevTime) and (t<=s.nextTime));
if not done then
begin
if t>s.prevTime then st:=si
else if t<s.nextTime then sb:=si;
if st=sb then done:=true;
end;
until done;
// do what you want with s
end;
Another option would be to create your own markup that you parse for that contains both the text and the delay timing. While a timer would work, the problem is that its not going to be accurate enough over time to give you reliable results since its fired based on messaging. Instead, I would perform triggers based on how far from the beginning of the music file you want the event to occur. This also allows the system to catch-up if some other app blocking process gets in the way and should help keep things in sync.
Something as simple as:
00:00:15;LYRIC;This is lyric line 1
00:00:18;FADEOUT
you then can parse this into list of appropriate objects which take the appropriate actions.
You should create a new class based on TGraphicControl/TCustomControl(anything with a canvas) and add a string property, now you have to create a timer as a private variable with it's interval value published through your class, something like so
...
type TLyricViewer = class(TGraphicControl)
private
FTimer : TTimer;
FLyric : string;
FBitmap : TBitmap;// offset bitmap on which you draw
// some more variables to store paint information
private
procedure OnNextWord(Sender: TObject);// assign this to FTimer.OnTimer event
public
constructor Create(AOwner: TComponent);
destructor Destroy; override;
public
procedure StartLyric;
procedure StopLyric;
procedure Paint; override;
published
property WordInterval : Integer|Cardinal
read GetWordInterval write SetWordInterval;
end;
...
procedure TLyricViewer.Paint;
begin
// here is where the magic happends
end;
constructor TLyricViewer.Create(AOwner: TComponent);
begin
// create timer, bitmap and set default properties
end;
destructor TLyricViewer.Destroy;
begin
// free and nil the timer and bitmap
inherited Destroy;
end;
The rest is up to you, after all your the one getting paid, work for it :)
Related
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 5 years ago.
Improve this question
I have a form class TfrmWelcome and I want to be able to dynamically add
a memo to it when a button is clicked in the main part of the form (frmWelcome.MainPanelSourceEditor).
My unsuccessful attempt at some code is below. I get the error
"undeclared identifier WelcomeMemo". How could I get this to compile and work?
type
WelcomeMemo : TMemo;
end;
implementation
procedure SetHelpWelcome;
begin
WelcomeMemo : TMemo.Create(frmWelcome);
with TMemo(FindComponent('WelcomeMemo')) do
begin
Parent := frmWelcome.MainPanelSourceEditor;
If what you are trying to do is to add a memo to your frmWelcome at runtime, a better (but still not very good) way to do it would be like this:
procedure SetHelpWelcome;
var
WelcomeMemo : TMemo;
begin
WelcomeMemo := TMemo.Create(frmWelcome);
WelcomeMemo.Parent := frmWelcome.MainPanelSourceEditor;
// set any other properties of WelcomeMemo here.
end;
This avoids the with (which you should never use especially if you are a beginner) and the completely avoidable FindComponent to find something you don't need to find in the first place if you capture it by the assignment to the WelcomeMemo local variable.
But that's still a fairly naff way of doing what you want. It would be better to have the WelcomeMemo as a member of your form, and define a method of the form to create and initialise it; you could then call the method from the OnClick handler of the button you want to use to create it. Something like (untested)
TfrmWelcome = Class(TForm)
private
fWelcomeMemo : TMemo;
procedure SetUpWelcomeMemo;
[...]
end;
procedure TfrmWelcome.SetUpWelcomeMemo;
begin
if fWelcomeMemo <> Nil then exit; // to avoid creating it more than once
fWelcomeMemo := TMemo.Create(Self);
fWelcomeMemo.Parent := Self.MainPanelSourceEditor;
// set any other properties of WelcomeMemo here.
end;
Apart from anything else, this avoids the memo's owner being set to the specific TfrmWelcome instance frmWelcome, which is an accident waiting to happen because it may not be the instance you are actually wanting to work with.
But like #J.. said, you really need to look at a beginner's tutorial if you are blundering around using trial and error the way it sounds like you are.
I am currently working on one a little bit complex application and I have found one problem. I am going to try to reduce complexity of my application into one simple application, just for example. The point is, that I have 16 regions and there is variables and procedures for each one of them. Each procedure have to be universal for each region. Currently I resolve that by writing one "if" on the beginning of the procedure and them copy that 15 times bellow and changing it little bit, because for each region it makes difference just in a few words. So I have to change that word in each one of them. This makes the code sooo unclear and it is wasting of time. Is there any way, how to write those 16 "ifs" into one? Using something like template or something like that?
Example application:
key code:
procedure TForm1.WriteItem;
var item:integer;
begin
if currentFile='FirstFile' then begin
Seek(FirstFile,filesize(firstfile)-1);
read(FirstFile,item);
inc(item);
write(FirstFile,item);
end;
if currentFile='SecondFile' then begin
Seek(SecondFile,filesize(SecondFile)-1);
read(SecondFile,item);
inc(item);
write(SecondFile,item);
end;
end;
full version:
https://drive.google.com/drive/folders/0BzhR4bZa5iBuazJuX0FWQzBXcHM?usp=sharing
I am guessing that FirstFile, SecondFile and so on are all of type TFile or some descendant of it, so the first change I would make would be to make 'currentFile' of the same type (or ancestor of it). Then, instead of setting your currentFile as a string, you put something like
currentFile := FifthFile;
for instance.
Then your procedure just becomes
procedure TForm1.WriteItem;
var item:integer;
begin
Seek(CurrentFile,filesize(CurrentFilefile)-1);
read(CurrentFile,item);
inc(item);
write(CurrentFile,item);
end;
Better, though, you could pass your file as a parameter, like this
procedure TForm1.WriteItem( const CurremtFile : TYourFileType);
var item:integer;
begin
Seek(CurrentFile,filesize(CurrentFilefile)-1);
read(CurrentFile,item);
inc(item);
write(CurrentFile,item);
end;
Edit
As pointed out in the comments, this procedure does not required any object variables (although your real procedure may). You can make this independent of any object one of two ways: Either move the function out of the object altogether
procedure WriteItem( const CurremtFile : TYourFileType);
or make it a class procedure
class procedure TForm1.WriteItem( const CurremtFile : TYourFileType);
As a general principle I prefer the latter method, but I would probably move it to a different class specifically designed to handle this type of functionality (not my form).
Overview
This question is a second attempt based on this one I recently asked: How can I make a TList property from my custom control streamable?
Although I accepted the answer in that question and it worked, I soon realized that TCollection is not the solution or requirement I was looking for.
Requirements
To keep my requirements as simple and clear to understand as possible, this is what I am attempting to:
Derive a new custom control based on TCustomListBox
Replace the Items property with my own Items type, eg a TList.
The TList (Items property) will hold objects, each containing a caption and a image index property etc.
Ownerdraw my listbox and draw its icons and text etc.
Create a property editor to edit the Items at design-time.
With that in mind, I know how to create the custom control, I know how to work with TList or even TObjectList for example, I know how to ownerdraw the control and I also know how to create the property editor.
Problem
What I don't know is how to replace the standard listbox Items type with my own? well I kind of do (publishing my own property that shares the same name), only I need to make sure it is fully streamable with the dfm.
I have searched extensively on this subject and have tried studying code where TListView and TTreeView etc publishes its Items type but I have found myself more confused than ever.
In fact I came across this very old question asked by someone else on a different website which asks very much what I want to do: Streaming a TList property of a component to a dfm. I have quoted it below in the event the link is lost:
I recently wrote a component that publishes a TList property. I then created a property editor for the TList to enable design-time editing. The problem is that the TList doesn't stream to the dfm file, so all changes are lost when the project is closed. I assume this is because TList inherits from TObject and not from TPersistant. I was hoping there was an easy work around for this situation (or that I have misunderstood the problem to begin with). Right now all I can come up with is to switch to a TCollection or override the DefineProperties method. Is there any other way to get the information in the TList streamed to and from the dfm?
I came across that whilst searching keywords such as DefineProperties() given that this was an alternative option Remy Lebeau briefly touched upon in the previous question linked at the top, it also seemed to be the answer to that question.
Question
I need to know how to replace the Items (TStrings) property of a TCustomListBox derived control with my own Items (TList) or Items (TObjectList) etc type but make it fully streamable with the dfm. I know from previous comments TList is not streamable but I cannot use TStrings like the standard TListBox control does, I need to use my own object based list that is streamable.
I don't want to use TCollection, DefineProperties sounds promising but I don't know how exactly I would implement this?
I would greatly appreciate some help with this please.
Thank you.
Override DefineProperties procedure in your TCustomListBox (let's name it TMyListBox here). In there it's possible to "register" as many fields as you wish, they will be stored in dfm in the same way as other fields, but you won't see them in object inspector. To be honest, I've never encountered having more then one property defined this way, called 'data' or 'strings'.
You can define 'normal' property or binary one. 'Normal' properties are quite handy for strings, integers, enumerations and so on. Here is how items with caption and ImageIndex can be implemented:
TMyListBox = class(TCustomListBox)
private
//other stuff
procedure ReadData(reader: TReader);
procedure WriteData(writer: TWriter);
protected
procedure DefineProperties(filer: TFiler); override;
//other stuff
public
//other stuff
property Items: TList read fItems; //not used for streaming, not shown in object inspector. Strictly for use in code itself. We can make it read-only to avoid memory leak.
published
//some properties
end;
that's DefineProperties implementation:
procedure TMyListBox.DefineProperties(filer: TFiler);
begin
filer.DefineProperty('data', ReadData, WriteData, items.Count>0);
end;
fourth argument, hasData is Boolean. When your component is saved to dfm, DefineProperties is called and it's possible to decide at that moment is there any data worth saving. If not, 'data' property is omitted. In this example, we won't have this property if there is no items present.
If we expect to ever use visual inheritance of this control (for example, create a frame with this listBox with predefined values and then eventually change them when put to form), there is a possibility to check, is value of this property any different than on our ancestor. Filer.Ancestor property is used for it. You can watch how it's done in TStrings:
procedure TStrings.DefineProperties(Filer: TFiler);
function DoWrite: Boolean;
begin
if Filer.Ancestor <> nil then
begin
Result := True;
if Filer.Ancestor is TStrings then
Result := not Equals(TStrings(Filer.Ancestor))
end
else Result := Count > 0;
end;
begin
Filer.DefineProperty('Strings', ReadData, WriteData, DoWrite);
end;
This would save a little bit of space (or lots of space if image is stored within) and sure is elegant, but in first implementation it can well be omitted.
Now the code for WriteData and ReadData. Writing is much easier usually and we may begin with it:
procedure TMyListBox.WriteData(writer: TWriter);
var i: Integer;
begin
writer.WriteListBegin; //in text dfm it will be '(' and new line
for i:=0 to items.Count-1 do begin
writer.WriteString(TListBoxItem(items[I]).caption);
writer.WriteInteger(TListBoxItem(items[I]).ImageIndex);
end;
writer.WriteListEnd;
end;
In dfm it will look like this:
object MyListBox1: TMyListBox
data = (
'item1'
-1
'item2'
-1
'item3'
0
'item4'
1)
end
Output from TCollection seems more elegant to me (triangular brackets and then items, one after another), but what we have here would suffice.
Now reading it:
procedure TMyListBox.ReadData(reader: TReader);
var item: TListBoxItem;
begin
reader.ReadListBegin;
while not reader.EndOfList do begin
item:=TListBoxItem.Create;
item.Caption:=reader.ReadString;
item.ImageIndex:=reader.ReadInteger;
items.Add(item); //maybe some other registering needed
end;
reader.ReadListEnd;
end;
That's it. In such a way rather complex structures can be streamed with ease, for example, two-dimensional arrays, we WriteListBegin when writing new row and then when writing new element.
Beware of WriteStr / ReadStr - these are some archaic procedures which exist for backward compatibility, ALWAYS use WriteString / ReadString instead!
Other way to do is to define binary property. That's used mostly for saving images into dfm. Let's say, for example, that listBox has hundreds of items and we'd like to compress data in it to reduce size of executable. Then:
TMyListBox = class(TCustomListBox)
private
//other stuff
procedure LoadFromStream(stream: TStream);
procedure SaveToStream(stream: TStream);
protected
procedure DefineProperties(filer: TFiler); override;
//etc
end;
procedure TMyListBox.DefineProperties(filer: TFiler);
filer.DefineBinaryProperty('data',LoadFromStream,SaveToStream,items.Count>0);
end;
procedure TMyListBox.SaveToStream(stream: TStream);
var gz: TCompressionStream;
i: Integer;
value: Integer;
item: TListBoxItem;
begin
gz:=TCompressionStream.Create(stream);
try
value:=items.Count;
//write number of items at first
gz.Write(value, SizeOf(value));
//properties can't be passed here, only variables
for i:=0 to items.Count-1 do begin
item:=TListBoxItem(items[I]);
value:=Length(item.Caption);
//almost as in good ol' Pascal: length of string and then string itself
gz.Write(value,SizeOf(value));
gz.Write(item.Caption[1], SizeOf(Char)*value); //will work in old Delphi and new (Unicode) ones
value:=item.ImageIndex;
gz.Write(value,SizeOf(value));
end;
finally
gz.free;
end;
end;
procedure TMyListBox.LoadFromStream(stream: TStream);
var gz: TDecompressionStream;
i: Integer;
count: Integer;
value: Integer;
item: TListBoxItem;
begin
gz:=TDecompressionStream.Create(stream);
try
gz.Read(count,SizeOf(count)); //number of items
for i:=0 to count-1 do begin
item:=TListBoxItem.Create;
gz.Read(value, SizeOf(value)); //length of string
SetLength(item.caption,value);
gz.Read(item.caption[1],SizeOf(char)*value); //we got our string
gz.Read(value, SizeOf(value)); //imageIndex
item.ImageIndex:=value;
items.Add(item); //some other initialization may be needed
end;
finally
gz.free;
end;
end;
In dfm it would look like this:
object MyListBox1: TMyListBox1
data = {
789C636260606005E24C86128654865C064386FF40802C62C40002009C5607CA}
end
78 is sort of signature of ZLib, 9C means default compression, so it works (there are only 2 items actually, not hundreds). Of course, this is just one example, with BinaryProperties any possible format may be used, for example saving to JSON and putting it into stream, or XML or something custom. But I'd not recommend to use binary unless it's absolutely inevitable, because it's difficult to see from dfm, what happens in component.
It seems like good idea to me to actively use streaming when implementing component: we can have no designer at all and set all values by manually editing dfm and see if component behaves correctly. Reading/loading itself can be tested easily: if component is loaded, then saved and text is just the same, it's all right. It's so 'transparent' when streaming format is 'human-readable', self-explaining that it often overweighs drawbacks (like file size) if there are any.
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 7 years ago.
Improve this question
I have a program that needs to process files at batch. Instead of showing errors in message boxes on screen (which will pause the execution of the program) I need to show those error messages in a Log that the user can see as the program executes.
So I DON'T need a program-execution log like this Which logging library is better?
I am using now something derived from TRichEdit. Basically, a rich edit with few extra methods like AddError(s), AddWarn(s), AddVerbose(s), etc.
TRichLog = class(TMyRichEdit)
private
protected
Indent: Integer; { Indent new added lines x spaces }
public
constructor Create(AOwner: TComponent); override;
procedure AddBold (CONST Mesaj: string);
procedure AddMsg (CONST Mesaj: string);
procedure AddMsgLvl (CONST Mesaj: string; MsgType: Integer);
procedure AddColorMsg (CONST Mesaj: string; Culoare: TColor);
procedure AddVerb (CONST Mesaj: string);
procedure AddHint (CONST Mesaj: string);
procedure AddInfo (CONST Mesaj: string);
procedure AddPath (CONST Mesaj: string);
procedure AddWarn (CONST Mesaj: string);
procedure AddError (CONST Mesaj: string);
procedure AddMsgInt (CONST Mesaj: string; i: Integer); { Adds a message text followed by an integer }
procedure AddInteger (CONST i: Integer);
procedure AddFromFile (CONST FileName: string; MsgType: Integer); { Reads lines from the specified file and adds them to the log using the specified color. }
procedure AddEmptyRow;
procedure AddDateStamp;
procedure Append (RamLog: TObject); { RamLog will be typecased to TRamLog }
procedure SaveAsRtf (CONST FullPath: string);
procedure AppendToFile(CONST FullPath: string);
function VerbosityAsString: string;
published
property InsertTime: Boolean read FInsertTime write FInsertTime default FALSE;
property InsertDate: Boolean read FInsertDate write FInsertDate default FALSE;
property AutoScroll: Boolean read FAutoScroll write FAutoScroll default TRUE; { Automatically scroll to show the last line }
property Verbosity : Integer read FVerbosity write FVerbosity default vHints;
property OnLogError: TNotifyEvent read FLogError write FLogError; { Can be used to inform the application to automatically switch to log when an error is listed }
property OnLogWarn : TNotifyEvent read FLogWarn write FLogWarn;
But I would like to let user dynamically filter the context. For example the user should be able to hide all Verbose messages and keep only the warnings and errors. And if the user changes his mind, to put back the verbose messages.
Can the (existing) text in RichEdit be filtered this way? If not, I would like to get some pointers about how to reimplement it. I am thinking about writing my own format to keep the lines/messages. For example:
Cannot open file,#msgErr,#Bold
The I would have a TStringGrid to display only a limited number of lines (the ones that are visible on screen). This way I could have millions of lines without actually rendering all of them on screen. Time wasted parsing won't matter since I only have to parse the visible lines.
Requirements:
Support for colors as in RichEdit (red for errors, etc)
Lightweight
Should have two classes: a visual one (based on TStringGrid) and one non-visual for console programs (log to RAM and save the log later or display the messages as simple text in the console).
No hard dependencies (Delphi edition, Indy, DB engines, 3rd party controls etc)
Small (TRichEdit increases the size of the EXE file quite a lot)
One PAS file
An alternative would be not to use the Grid and to draw the text myself (for example in a TPanel-derived component). Or maybe such control already exists.
Any constructive critics of my ideas would be welcome. Do you have a better idea than using a Grid?
IMHO we may make a difference between:
Low-level logging, which targets developers and support;
High-level logging, which targets end-users.
From your comments, sounds like if you need the 2nd kind, which is usually also called "Audit Trail", especially in terms or regulation.
We usually implement high-level "Audit Trail" by storing the events in a database. For instance, a local high-performance SQLite3 database, or a MongoDB centralized instance.
Using a RDBMS (or NoSQL DB) has several advantages:
Its structure could be at the same time fixed (e.g. by defining categories), and evolutive (via some text fields, or even some foreign keys with other tables);
It is easy to interface with your existing UI, e.g. using powerful third-party grids, with sorting and filtering abilities;
You could use SQL to search within the log content, then even define a dedicated UI for most useful cases;
It is easy to maintain (a DELETE FROM ... would get rid of older undeded entries, and potentially keeping errors in the database).
We usually do this on production, using our Open Source SOA framework: all service calls can be directly written in a SQlite3 or MongoDB instance, without any code to write! Then you could even search within the parameters using JSON queries. And you still have integrated low-level logging available.
I'm new to Stack Overflow but I find myself seeking some of the best programming solutions on this site. So I have a question to ask.
I am writing a program in Delphi that is a TUI menu-driven program for a local business client. They have asked me to keep the user interface the same as in the old program (written in BASIC for MS-DOS, dated in 1982) so it is all menu driven with global data being stored in files and reloaded by the program. Each sub-menu is a program in and of itself run by the active menu (also a program).
I have written my own TUI framework and UI manager for displaying menus and sub-menus. The UI manager contains an overridden method called "Draw" to display the menu and another overridden method called "OnEvent" which handles keyboard events in the UI. My first question is would you consider this to be an appropriate method for making a menu-driven program containing sub-menus? An example of how this works is such:
type
TMenu1 = class(TExtendedUIManager)
private
procedure OnEvent (c: Char); override;
end;
type
TSubMenu1 = class(TExtendedUIManager)
end;
procedure TMenu1.OnEvent (c: Char);
var
Next: TExtendedUIManager;
begin
if c = '2' then begin
Next := TSubMenu1.Create;
Self.Start(Next);
Next.Free;
end;
end;
My other question is what would be an appropriate way of sharing data between menus? For example, if I wanted my TSubMenu1 class to return a string when a method is called, how would I make it accessible to other sub-menus that do not interact with it? (Sorry if the question is vague). I have the Singleton pattern in mind but I've also thought of having the UI manager store a reference to some object for data storage and each time a new sub-menu is run, pass in the reference to the new sub-menu (UI manager). The conundrum is finding out which one works best. Or even if my menu-driven framework is decent.
Opinions are welcomed and any advice is appreciated. Thanks for your time and help!
--Todd
"My other question is what would be an appropriate way of sharing data between menus?"
You can share the data by using class methods and properties. By using these, you can even access them even without create instance of the class. For more info go through this Link.
Below is sample code which will share List.
type
TForm1 = class(TForm)
---
---
private
{ Private declarations }
class var List: TStringList;
---
end;
var
Form1, Form2: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
Form1.List.Add('4');
Form2.List.Add('5');
ShowMessage(TForm1.List.Text);
end;
initialization
StrList := TStringList.Create;
TForm1.List := TStringList.Create;
TForm1.List.Add('1');
TForm1.List.Add('2');
TForm1.List.Add('3');
ShowMessage(TForm1.List.Text);
finalization
FreeAndNil(TForm1.List);
end.
I'll freely admit this is thinking aloud territory for me, but the first thing that caught my eye was this:
procedure TMenu1.OnEvent (c: Char);
var
Next: TExtendedUIManager;
begin
if c = '2' then begin
Next := TSubMenu1.Create;
Self.Start(Next);
Next.Free;
end;
end;
It feels like the end condition of this sort of programming is going to be huge decision trees of if c = '2' then ... else if c = '3' then ... else and so on. This does get tedious to write and maintain; if feasible, an array mapping input character with a function to execute is often far easier to maintain.
[['2', foo_create],
['3', foo_delete],
['4', foo_ship],
['d', foo_destroy_all],
['`', foo_return_to_previous_menu]]
When a new character comes in, you would look up the corresponding function in the table and execute it. Once you're done, return to waiting.
You could further extend this array to keep track of arguments that are required and values returned from each function. When you execute a function, store its return value in a global variable somewhere, and keep track of what types were returned. (Perhaps Delphi's type system is sophisticated enough that it is trivial, and not even worth mentioning.) When you execute a new function, check to see if the type of the 'current' returned result is suitable to pass to the desired function. (You could even grey-out the menu entries if the types are wrong, to indicate to the user that this combination won't work.)
/* key function arg ret types */
[['2', foo_create, NULL, FOO],
['3', foo_delete, FOO, NULL],
['4', foo_ship, FOO, NULL],
['d', foo_destroy_all, NULL, NULL],
['`', foo_return_to_previous_menu, NULL, NULL]]
I hope this is useful in some fashion. :)