Author Topic: AlphaKey.cpp (find unique menu shortcuts).  (Read 5453 times)

Offline orefa

  • Multiple posting newcomer
  • *
  • Posts: 102
AlphaKey.cpp (find unique menu shortcuts).
« on: April 06, 2006, 05:01:06 am »
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.

Code
//------------------------------------------------------------------------------
// 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;
}