News:

Accounts with zero posts and zero activity during the last months will be deleted periodically to fight SPAM!

Main Menu

AlphaKey.cpp (find unique menu shortcuts).

Started by orefa, April 06, 2006, 05:01:06 AM

Previous topic - Next topic

orefa

A small annoyance for me is the lack of unique shortcut letters in menus: some are duplicates, some are absent. It can be tedious to search for a unique letter for each item, especially when menus are long and many items have letters in common. So I wrote a small console-based utility to do this (below) for all to use as they see fit, including the Code::Blocks developpers of course. ;-)

Enter titles one at a time, either from the console or a redirected data file. The program also provides useful information like a table of letters used, available but unused shortcut candidates and unused letters to help vary the actual titles in order to achieve uniqueness. For example, a little bit of fiddling assisted by this utility yielded this reworked Code::Blocks "File" menu:

n: (N)ew file
e: N(e)w project

o: (O)pen
f: Recent (f)iles
r: (R)ecent projects

s: (S)ave file
a: S(a)ve project
v: Sa(v)e workspace
y: Save ever(y)thing

i: Save f(i)le as
j: Save pro(j)ect as
w: Save (w)orkspace as

c: (C)lose file
l: C(l)ose project
k: Close wor(k)space

p: (P)rint
x: E(x)port

u: R(u)n script

t: Proper(t)ies

q: (Q)uit

Changed: from "save all files" to "save everything" because:
- not enough unique shortcut letters,
- projects and workspaces are not edited like "files",
- it's even better to save all files, projects and workspace in one go.

Deleted: "close all", which is redundant with close file, close project and close workspace. Alternatively, use "h: close everyt(h)ing" to match "save everything".

Well, hopefully someone will find this useful. I think this is the right place to post it since the intent was to help developpers polish up the Code::Blocks menus.


//------------------------------------------------------------------------------
// AlphaKey
//
// This program takes as input a list of "titles" that may be used in menus
// or dialog boxes and attempts to identify a unique alphabetic key from 'a'
// to 'z' which can be used to uniquely identify each title. The purpose is
// to select the best letter as a shortcut to the title in question (which
// would be underlined in a Windows application).
//
// Obviously, there is an input limit of 26 titles. The program will fail to
// find a unique key for each title if there are less unique letters used in
// the list than there are titles. Even if there are enough unique letters,
// the search may still fail if they are not distributed so that each title
// indeed has at least one letter that can be uniquely its own. For example
// this list: "aa", "bb", "ab" and "xy" holds 4 unique letters in 4 titles
// yet does not have unique keys for each of the first 3 items.
//
// The search is exhaustive, all possible combinations are examined, which
// can take a while for larger title sets. The search is fast if a solution
// exists and the titles are already listed in the "most desirable" order,
// that is items that have a key early on in the title appear first. If the
// first title's only possible key is the last letter of the word, then all
// unsuccessful possibilities must first be ruled out starting at the first
// letter. Avoiding this by re-ordering the input prevents such worst-case
// scenarios.
//
// Despite an occasional worst-case run, an exhaustive search is good to
// provide the "best order" for keys since the first successful letter
// appears early in early titles for a fairly natural key selection.
//
// Tips & Tricks
//
// Reserve letter(s) that must be assigned to a specific titles by entering
// these single letters first followed by a '|' character. All letters after
// this will be ignored in the search. For efficiency, enter these first in
// the input list. Example: enter "s|Save" and "a|Save As" to reserve these
// letter keys for these titles. A few favorite letters can also be specified
// with "saf|Save file" and "av|Save As" for example.
//
// If a search takes too long, look for the presence of a unique letter in all
// titles (print the table to see). Reserve the unique letter for this title
// (as above), then repeat the search.
//
// Moving titles around a little can speed up the search process and/or
// produce alternate selections. Vary the input order.
//
//------------------------------------------------------------------------------

#include <cctype>
#include <cstdio>
#include <cstring>

//------------------------------------------------------------------------------
// Work data.

char titles[27][100];       // List of titles.
int  titleCount;            // Input count.
char streamlined[26][27];   // Unique lowercase letters in each title.
char keys[26];              // Resulting letter key for each title.
bool used[26];              // Work flags for letters already used.

//------------------------------------------------------------------------------

/* Read a line "cleanly" and return its length. */
int strget(char *destination, int bufferSize, FILE *inStream) {
    char *str = destination;
    int c;
    // Skip leading whitespace.
    do {
        c = getc(inStream);
    } while (c != EOF && c != '\n' && isspace(c));
    // Read until the end or until the buffer is full.
    while (c != EOF && c != '\n' && bufferSize > 1) {
        *str++ = (char) c; bufferSize--;
        c = getc(inStream);
    }
    // Terminate and trim trailing whitespace.
    do {
        *str-- = '\0';
    } while (str >= destination && isspace(*str));
    // Clear out the input buffer.
    while (c != EOF && c != '\n') {
        c = getc(inStream);
    }
    // Return the input length.
    return str - destination + 1;
}

// Read a list of titles, one title per line. Return true if a list of up
// to 26 titles was provided. If the list is empty or exceeds 26, issue an
// error message and return false.
bool inputTitles() {
    titleCount = 0;
    while (strget(titles[titleCount], 100, stdin)) {
        if (++titleCount > 26) {
            fprintf(stderr, "Error: More than 26 titles.\n");
            return false;
        }
    }
    if (titleCount == 0) {
        fprintf(stderr, "Error: No data.\n");
        return false;
    }
    return true;
}

// For efficient processing, streamline a title by copying into 'dst' only
// unique lowercase letters from 'title'. Preserve ordering of used letters.
void streamline(char* dst, const char* title) {
    char const * DST = dst;
    while (*title && (*title != '|')) {    // Ignore what follows '|'
        if (isalpha(*title)) {
            char letter = tolower(*title);

            const char* cp = DST;           // Is this letter already in dst?
            while (cp < dst && letter != *cp) {
                ++cp;
            }

            if (cp == dst) {                // No prior duplicate in 'dst':
                *dst = letter;              //    store this unique letter
                ++dst;                      //    and advance position.
            }
        }
        ++title;                            // Next title character.
    }
    *dst = '\0';                            // End the letter array as a string.
}

// Set streamlined versions of all titles.
void streamlineAll() {
    for (int i = 0; i < titleCount; ++i) {
        streamline(streamlined[i], titles[i]);
    }
}

// Clear all 'used' flags.
void clearUsed() {
    for (int i = 0; i < 26; ++i) {
        used[i] = false;
    }
}

// Mark all letters used in the (streamlined) title at index 'i'.
void markUsedIn(int i) {
    for (int j = 0; streamlined[i][j]; ++j) {
        used[streamlined[i][j] - 'a'] = true;
    }
}

// Mark all letters used in all titles.
void markUsedInAll() {
    clearUsed();
    for (int i = 0; i < titleCount; ++i) {
        markUsedIn(i);
    }
}

// Set all unique letters used and return their count.
int uniqueLetters() {
    int count = 0;
    markUsedInAll();
    for (int i = 0; i < 26; ++i) {
        if (used[i]) ++count;
    }
    return count;
}

//------------------------------------------------------------------------------
// Main recursive search function.

bool findUniqueAlpha(int titleIndex = 0) {
    // If the title index has passed the end then the search was successful.
    if (titleIndex == titleCount) {
        return true;
    }
    // Try using each letter in turn as a key for this title.
    for (char* letter = streamlined[titleIndex]; *letter; ++letter) {
        // Progress report.
        if (titleIndex < 4) {
            printf("Searching...");
            // Show nesting level visually using spaces.
            for (int i = 0; i < titleIndex; ++i) {
                printf(" ");
            }
            printf("%c\n", *letter);
        }
        // Try this letter as key if it is not already in use.
        int index = *letter - 'a';
        if (!used[index]) {
            used[index] = true;
            if (findUniqueAlpha(titleIndex + 1)) {  // Recurse with next title.
                keys[titleIndex] = *letter;         // Success, store the key.
                return true;                        // Leave the flag set.
            }
            used[index] = false;                    // Clear flag, continue.
        }
    }
    // No solution found for this title given all letters already used as key.
    return false;
}

//------------------------------------------------------------------------------
// Reporting functions

// Show letters either used or not used.
void showUsed(bool usage, const char* header = 0) {
    if (header) {
        printf(header);
    }
    for (int i = 0; i < 26; ++i) {
        if (used[i] == usage) {
            printf(" %c", char('a' + i));
        }
    }
}

// Show a table of letters used by each title.
void showTable() {
    for (int i = 0; i < titleCount; ++i) {
        // Mark each letter used by this title alone.
        clearUsed();
        markUsedIn(i);
        // Show 26 markers, either a used letter or a delimiting character.
        for (int j = 0; j < 26; ++j) {
            printf("%c", used[j] ? char('a' + j) : '.');
        }
        // End the line with the corresponding title.
        printf("  %s\n", titles[i]);
    }
    printf("\n");
}

// Show all keys and their corresponding titles.
void showKeys() {
    for (int i = 0; i < titleCount; ++i) {
        printf("%c: %s\n", keys[i], titles[i]);
    }
}

// Show letters used in titles but not in keys.
void showAvailableKeys(const char* header = 0) {
    // Check each used letter against the set of keys.
    markUsedInAll();
    int count = 0;
    for (int i = 0; i < 26; ++i) {
        if (used[i]) {
            char letter = char('a' + i);
            // Clear the flag is this letter is used in a key.
            for (int j = 0; j < titleCount; ++j) {
                if (keys[j] == letter) {
                    used[i] = false;
                    break;
                }
            }
            // Is this letter still available for use as a key?
            if (used[i]) {
                ++count;
            }
        }
    }
    // Report on available letters only if there are any.
    if (count) {
        showUsed(true, header);
    }
}

int showHelp() {
    printf("\n"
           "AlphaKey requires input of one title per line, each one up to\n"
           "99 characters in length. Input an empty line (press Enter alone)\n"
           "to end data entry.\n"
           "\n"
           "Use \"AlphaKey -table\" to just see a table of letters used by\n"
           "each title provided as input.\n"
           "\n"
           "Use character | to force the selection of any letter before it\n"
           "as key for the title. Letters after | are ignored. Example: use\n"
           "\"fn|Family Name\" to force selection of either f or n as key.\n");
    return 0;
}

//------------------------------------------------------------------------------

int main(int argc, char* argv[]) {
    // Check command line arguments.
    bool wantTable = (argc == 2)
                  && (*argv[1] == '-' || *argv[1] == '/')
                  && (strcmp(argv[1] + 1, "table") == 0);
    if (argc != 1 && !wantTable) {
        return showHelp();
    }
    // Process the input, if any.
    if (inputTitles()) {
        // All accessory functions use array 'streamlined' instead of 'titles'.
        streamlineAll();

        if (wantTable) {
            showTable();
        }
        else {
            int uniqueCount = uniqueLetters();
            if (uniqueCount < titleCount) {
                // Error situation, not enough unique letters.
                showTable();
                printf("\nOnly %d unique letters in %d different titles.\n",
                       uniqueCount, titleCount);
                showUsed(false, "\nUnused letters:");
            }
            else {
                // Start searching (clear 'used' first).
                clearUsed();
                if (findUniqueAlpha()) {
                    printf("\n\n\n");
                    showTable();
                    showKeys();
                    showAvailableKeys("\nAvailable keys:");
                    if (titleCount < 26) {
                        markUsedInAll();
                        showUsed(false, "\nUnused letters:");
                    }
                }
                else {
                    showTable();
                    printf("\nNo solution found.\n");
                }
            }
            printf("\n");
        }
    }
    return 0;
}