1 /* 2 * Copyright 1999-2009 Jeremy Friesner 3 * Copyright 2009-2010 Haiku, Inc. All rights reserved. 4 * Distributed under the terms of the MIT License. 5 * 6 * Authors: 7 * Jeremy Friesner 8 */ 9 10 #include "ShortcutsSpec.h" 11 12 #include <ctype.h> 13 #include <stdio.h> 14 15 #include <Beep.h> 16 #include <Catalog.h> 17 #include <ColumnTypes.h> 18 #include <Directory.h> 19 #include <Locale.h> 20 #include <NodeInfo.h> 21 #include <Path.h> 22 #include <Region.h> 23 #include <Window.h> 24 25 #include "ColumnListView.h" 26 27 #include "BitFieldTesters.h" 28 #include "CommandActuators.h" 29 #include "KeyInfos.h" 30 #include "MetaKeyStateMap.h" 31 #include "ParseCommandLine.h" 32 33 34 #define CLASS "ShortcutsSpec : " 35 36 #undef B_TRANSLATION_CONTEXT 37 #define B_TRANSLATION_CONTEXT "ShortcutsSpec" 38 39 const float _height = 20.0f; 40 41 static MetaKeyStateMap sMetaMaps[ShortcutsSpec::NUM_META_COLUMNS]; 42 43 static bool sFontCached = false; 44 static BFont sViewFont; 45 static float sFontHeight; 46 47 const char* ShortcutsSpec::sShiftName; 48 const char* ShortcutsSpec::sControlName; 49 const char* ShortcutsSpec::sOptionName; 50 const char* ShortcutsSpec::sCommandName; 51 52 53 #define ICON_BITMAP_RECT BRect(0.0f, 0.0f, 15.0f, 15.0f) 54 #define ICON_BITMAP_SPACE B_RGBA32 55 56 57 // Returns the (pos)'th char in the string, or '\0' if (pos) if off the end of 58 // the string 59 static char 60 GetLetterAt(const char* str, int pos) 61 { 62 for (int i = 0; i < pos; i++) { 63 if (str[i] == '\0') 64 return '\0'; 65 } 66 return str[pos]; 67 } 68 69 70 // Setup the states in a standard manner for a pair of meta-keys. 71 static void 72 SetupStandardMap(MetaKeyStateMap& map, const char* name, uint32 both, 73 uint32 left, uint32 right) 74 { 75 map.SetInfo(name); 76 77 // In this state, neither key may be pressed. 78 map.AddState("(None)", new HasBitsFieldTester(0, both)); 79 80 // Here, either may be pressed. (Remember both is NOT a 2-bit chord, it's 81 // another bit entirely) 82 map.AddState("Either", new HasBitsFieldTester(both)); 83 84 // Here, only the left may be pressed 85 map.AddState("Left", new HasBitsFieldTester(left, right)); 86 87 // Here, only the right may be pressed 88 map.AddState("Right", new HasBitsFieldTester(right, left)); 89 90 // Here, both must be pressed. 91 map.AddState("Both", new HasBitsFieldTester(left | right)); 92 } 93 94 95 MetaKeyStateMap& 96 GetNthKeyMap(int which) 97 { 98 return sMetaMaps[which]; 99 } 100 101 102 /*static*/ void 103 ShortcutsSpec::InitializeMetaMaps() 104 { 105 static bool metaMapsInitialized = false; 106 if (metaMapsInitialized) 107 return; 108 metaMapsInitialized = true; 109 110 _InitModifierNames(); 111 112 SetupStandardMap(sMetaMaps[ShortcutsSpec::SHIFT_COLUMN_INDEX], sShiftName, 113 B_SHIFT_KEY, B_LEFT_SHIFT_KEY, B_RIGHT_SHIFT_KEY); 114 115 SetupStandardMap(sMetaMaps[ShortcutsSpec::CONTROL_COLUMN_INDEX], 116 sControlName, B_CONTROL_KEY, B_LEFT_CONTROL_KEY, B_RIGHT_CONTROL_KEY); 117 118 SetupStandardMap(sMetaMaps[ShortcutsSpec::COMMAND_COLUMN_INDEX], 119 sCommandName, B_COMMAND_KEY, B_LEFT_COMMAND_KEY, B_RIGHT_COMMAND_KEY); 120 121 SetupStandardMap(sMetaMaps[ShortcutsSpec::OPTION_COLUMN_INDEX], sOptionName 122 , B_OPTION_KEY, B_LEFT_OPTION_KEY, B_RIGHT_OPTION_KEY); 123 } 124 125 126 ShortcutsSpec::ShortcutsSpec(const char* cmd) 127 : 128 BRow(), 129 fCommand(NULL), 130 fBitmap(ICON_BITMAP_RECT, ICON_BITMAP_SPACE), 131 fLastBitmapName(NULL), 132 fBitmapValid(false), 133 fKey(0), 134 fCursorPtsValid(false) 135 { 136 for (int i = 0; i < NUM_META_COLUMNS; i++) 137 fMetaCellStateIndex[i] = 0; 138 SetCommand(cmd); 139 } 140 141 142 ShortcutsSpec::ShortcutsSpec(const ShortcutsSpec& from) 143 : 144 BRow(), 145 fCommand(NULL), 146 fBitmap(ICON_BITMAP_RECT, ICON_BITMAP_SPACE), 147 fLastBitmapName(NULL), 148 fBitmapValid(false), 149 fKey(from.fKey), 150 fCursorPtsValid(false) 151 { 152 for (int i = 0; i < NUM_META_COLUMNS; i++) 153 fMetaCellStateIndex[i] = from.fMetaCellStateIndex[i]; 154 155 SetCommand(from.fCommand); 156 SetSelectedColumn(from.GetSelectedColumn()); 157 158 for (int i = 0; i < from.CountFields(); i++) 159 SetField(new BStringField( 160 static_cast<const BStringField*>(from.GetField(i))->String()), i); 161 } 162 163 164 ShortcutsSpec::ShortcutsSpec(BMessage* from) 165 : 166 BRow(), 167 fCommand(NULL), 168 fBitmap(ICON_BITMAP_RECT, ICON_BITMAP_SPACE), 169 fLastBitmapName(NULL), 170 fBitmapValid(false), 171 fCursorPtsValid(false) 172 { 173 const char* temp; 174 if (from->FindString("command", &temp) != B_NO_ERROR) { 175 printf(CLASS); 176 printf(" Error, no command string in archive BMessage!\n"); 177 temp = ""; 178 } 179 180 SetCommand(temp); 181 182 if (from->FindInt32("key", (int32*) &fKey) != B_NO_ERROR) { 183 printf(CLASS); 184 printf(" Error, no key int32 in archive BMessage!\n"); 185 } 186 187 for (int i = 0; i < NUM_META_COLUMNS; i++) 188 if (from->FindInt32("mcidx", i, (int32*)&fMetaCellStateIndex[i]) 189 != B_NO_ERROR) { 190 printf(CLASS); 191 printf(" Error, no modifiers int32 in archive BMessage!\n"); 192 } 193 194 for (int i = 0; i <= STRING_COLUMN_INDEX; i++) 195 SetField(new BStringField(GetCellText(i)), i); 196 } 197 198 199 void 200 ShortcutsSpec::SetCommand(const char* command) 201 { 202 delete[] fCommand; 203 // out with the old (if any)... 204 fCommandLen = strlen(command) + 1; 205 fCommandNul = fCommandLen - 1; 206 fCommand = new char[fCommandLen]; 207 strcpy(fCommand, command); 208 SetField(new BStringField(command), STRING_COLUMN_INDEX); 209 } 210 211 212 const char* 213 ShortcutsSpec::GetColumnName(int i) 214 { 215 return sMetaMaps[i].GetName(); 216 } 217 218 219 status_t 220 ShortcutsSpec::Archive(BMessage* into, bool deep) const 221 { 222 status_t ret = BArchivable::Archive(into, deep); 223 if (ret != B_NO_ERROR) 224 return ret; 225 226 into->AddString("class", "ShortcutsSpec"); 227 228 // These fields are for our prefs panel's benefit only 229 into->AddString("command", fCommand); 230 into->AddInt32("key", fKey); 231 232 // Assemble a BitFieldTester for the input_server add-on to use... 233 MinMatchFieldTester test(NUM_META_COLUMNS, false); 234 for (int i = 0; i < NUM_META_COLUMNS; i++) { 235 // for easy parsing by prefs applet on load-in 236 into->AddInt32("mcidx", fMetaCellStateIndex[i]); 237 test.AddSlave(sMetaMaps[i].GetNthStateTester(fMetaCellStateIndex[i])); 238 } 239 240 BMessage testerMsg; 241 ret = test.Archive(&testerMsg); 242 if (ret != B_NO_ERROR) 243 return ret; 244 245 into->AddMessage("modtester", &testerMsg); 246 247 // And also create a CommandActuator for the input_server add-on to execute 248 CommandActuator* act = CreateCommandActuator(fCommand); 249 BMessage actMsg; 250 ret = act->Archive(&actMsg); 251 if (ret != B_NO_ERROR) 252 return ret; 253 delete act; 254 255 into->AddMessage("act", &actMsg); 256 257 return ret; 258 } 259 260 261 BArchivable* 262 ShortcutsSpec::Instantiate(BMessage* from) 263 { 264 bool validateOK = false; 265 if (validate_instantiation(from, "ShortcutsSpec")) 266 validateOK = true; 267 else // test the old one. 268 if (validate_instantiation(from, "SpicyKeysSpec")) 269 validateOK = true; 270 271 if (!validateOK) 272 return NULL; 273 274 return new ShortcutsSpec(from); 275 } 276 277 278 ShortcutsSpec::~ShortcutsSpec() 279 { 280 delete[] fCommand; 281 delete[] fLastBitmapName; 282 } 283 284 285 void 286 ShortcutsSpec::_CacheViewFont(BView* owner) 287 { 288 if (sFontCached == false) { 289 sFontCached = true; 290 owner->GetFont(&sViewFont); 291 font_height fh; 292 sViewFont.GetHeight(&fh); 293 sFontHeight = fh.ascent - fh.descent; 294 } 295 } 296 297 298 const char* 299 ShortcutsSpec::GetCellText(int whichColumn) const 300 { 301 const char* temp = ""; // default 302 switch (whichColumn) { 303 case KEY_COLUMN_INDEX: 304 { 305 if ((fKey > 0) && (fKey <= 0xFF)) { 306 temp = GetKeyName(fKey); 307 if (temp == NULL) 308 temp = ""; 309 } else if (fKey > 0xFF) { 310 sprintf(fScratch, "#%" B_PRIx32, fKey); 311 return fScratch; 312 } 313 break; 314 } 315 316 case STRING_COLUMN_INDEX: 317 temp = fCommand; 318 break; 319 320 default: 321 if ((whichColumn >= 0) && (whichColumn < NUM_META_COLUMNS)) 322 temp = sMetaMaps[whichColumn].GetNthStateDesc( 323 fMetaCellStateIndex[whichColumn]); 324 if (temp[0] == '(') 325 temp = ""; 326 break; 327 } 328 return temp; 329 } 330 331 332 bool 333 ShortcutsSpec::ProcessColumnMouseClick(int whichColumn) 334 { 335 if ((whichColumn >= 0) && (whichColumn < NUM_META_COLUMNS)) { 336 // same as hitting space for these columns: cycle entry 337 const char temp = B_SPACE; 338 339 // 3rd arg isn't correct but it isn't read for this case anyway 340 return ProcessColumnKeyStroke(whichColumn, &temp, 0); 341 } 342 return false; 343 } 344 345 346 bool 347 ShortcutsSpec::ProcessColumnTextString(int whichColumn, const char* string) 348 { 349 switch (whichColumn) { 350 case STRING_COLUMN_INDEX: 351 SetCommand(string); 352 return true; 353 break; 354 355 case KEY_COLUMN_INDEX: 356 { 357 fKey = FindKeyCode(string); 358 SetField(new BStringField(GetCellText(whichColumn)), 359 KEY_COLUMN_INDEX); 360 return true; 361 break; 362 } 363 364 default: 365 return ProcessColumnKeyStroke(whichColumn, string, 0); 366 } 367 } 368 369 370 bool 371 ShortcutsSpec::_AttemptTabCompletion() 372 { 373 bool result = false; 374 375 int32 argc; 376 char** argv = ParseArgvFromString(fCommand, argc); 377 if (argc > 0) { 378 // Try to complete the path partially expressed in the last argument! 379 char* arg = argv[argc - 1]; 380 char* fileFragment = strrchr(arg, '/'); 381 if (fileFragment != NULL) { 382 const char* directoryName = (fileFragment == arg) ? "/" : arg; 383 *fileFragment = '\0'; 384 fileFragment++; 385 int fragmentLength = strlen(fileFragment); 386 387 BDirectory dir(directoryName); 388 if (dir.InitCheck() == B_NO_ERROR) { 389 BEntry nextEnt; 390 BPath nextPath; 391 BList matchList; 392 int maxEntryLen = 0; 393 394 // Read in all the files in the directory whose names start 395 // with our fragment. 396 while (dir.GetNextEntry(&nextEnt) == B_NO_ERROR) { 397 if (nextEnt.GetPath(&nextPath) == B_NO_ERROR) { 398 char* filePath = strrchr(nextPath.Path(), '/') + 1; 399 if (strncmp(filePath, fileFragment, fragmentLength) == 0) { 400 int len = strlen(filePath); 401 if (len > maxEntryLen) 402 maxEntryLen = len; 403 char* newStr = new char[len + 1]; 404 strcpy(newStr, filePath); 405 matchList.AddItem(newStr); 406 } 407 } 408 } 409 410 // Now slowly extend our keyword to its full length, counting 411 // numbers of matches at each step. If the match list length 412 // is 1, we can use that whole entry. If it's greater than one, 413 // we can use just the match length. 414 int matchLen = matchList.CountItems(); 415 if (matchLen > 0) { 416 int i; 417 BString result(fileFragment); 418 for (i = fragmentLength; i < maxEntryLen; i++) { 419 // See if all the matching entries have the same letter 420 // in the next position... if so, we can go farther. 421 char commonLetter = '\0'; 422 for (int j = 0; j < matchLen; j++) { 423 char nextLetter = GetLetterAt( 424 (char*)matchList.ItemAt(j), i); 425 if (commonLetter == '\0') 426 commonLetter = nextLetter; 427 428 if ((commonLetter != '\0') 429 && (commonLetter != nextLetter)) { 430 commonLetter = '\0';// failed; 431 beep(); 432 break; 433 } 434 } 435 if (commonLetter == '\0') 436 break; 437 else 438 result.Append(commonLetter, 1); 439 } 440 441 // free all the strings we allocated 442 for (int k = 0; k < matchLen; k++) 443 delete [] ((char*)matchList.ItemAt(k)); 444 445 DoStandardEscapes(result); 446 447 BString wholeLine; 448 for (int l = 0; l < argc - 1; l++) { 449 wholeLine += argv[l]; 450 wholeLine += " "; 451 } 452 453 BString file(directoryName); 454 DoStandardEscapes(file); 455 456 if (directoryName[strlen(directoryName) - 1] != '/') 457 file += "/"; 458 459 file += result; 460 461 // Remove any trailing slash... 462 const char* fileStr = file.String(); 463 if (fileStr[strlen(fileStr) - 1] == '/') 464 file.RemoveLast("/"); 465 466 // and re-append it iff the file is a dir. 467 BDirectory testFileAsDir(file.String()); 468 if ((strcmp(file.String(), "/") != 0) 469 && (testFileAsDir.InitCheck() == B_NO_ERROR)) 470 file.Append("/"); 471 472 wholeLine += file; 473 474 SetCommand(wholeLine.String()); 475 result = true; 476 } 477 } 478 *(fileFragment - 1) = '/'; 479 } 480 } 481 FreeArgv(argv); 482 483 return result; 484 } 485 486 487 bool 488 ShortcutsSpec::ProcessColumnKeyStroke(int whichColumn, const char* bytes, 489 int32 key) 490 { 491 bool result = false; 492 493 switch (whichColumn) { 494 case KEY_COLUMN_INDEX: 495 if (key > -1) { 496 if ((int32)fKey != key) { 497 fKey = key; 498 result = true; 499 } 500 } 501 break; 502 503 case STRING_COLUMN_INDEX: 504 { 505 switch (bytes[0]) { 506 case B_BACKSPACE: 507 case B_DELETE: 508 if (fCommandNul > 0) { 509 // trim a char off the string 510 fCommand[fCommandNul - 1] = '\0'; 511 fCommandNul--; // note new nul position 512 result = true; 513 } 514 break; 515 516 case B_TAB: 517 if (_AttemptTabCompletion()) { 518 result = true; 519 } else 520 beep(); 521 break; 522 523 default: 524 { 525 uint32 newCharLen = strlen(bytes); 526 if ((newCharLen > 0) && (bytes[0] >= ' ')) { 527 bool reAllocString = false; 528 // Make sure we have enough room in our command string 529 // to add these chars... 530 while (fCommandLen - fCommandNul <= newCharLen) { 531 reAllocString = true; 532 // enough for a while... 533 fCommandLen = (fCommandLen + 10) * 2; 534 } 535 536 if (reAllocString) { 537 char* temp = new char[fCommandLen]; 538 strcpy(temp, fCommand); 539 delete [] fCommand; 540 fCommand = temp; 541 // fCommandNul is still valid since it's an offset 542 // and the string length is the same for now 543 } 544 545 // Here we should be guaranteed enough room. 546 strncat(fCommand, bytes, fCommandLen); 547 fCommandNul += newCharLen; 548 result = true; 549 } 550 } 551 } 552 break; 553 } 554 555 default: 556 if (whichColumn < 0 || whichColumn >= NUM_META_COLUMNS) 557 break; 558 559 MetaKeyStateMap * map = &sMetaMaps[whichColumn]; 560 int curState = fMetaCellStateIndex[whichColumn]; 561 int origState = curState; 562 int numStates = map->GetNumStates(); 563 564 switch(bytes[0]) { 565 case B_RETURN: 566 // cycle to the previous state 567 curState = (curState + numStates - 1) % numStates; 568 break; 569 570 case B_SPACE: 571 // cycle to the next state 572 curState = (curState + 1) % numStates; 573 break; 574 575 default: 576 { 577 // Go to the state starting with the given letter, if 578 // any 579 char letter = bytes[0]; 580 if (islower(letter)) 581 letter = toupper(letter); // convert to upper case 582 583 if ((letter == B_BACKSPACE) || (letter == B_DELETE)) 584 letter = '('; 585 // so space bar will blank out an entry 586 587 for (int i = 0; i < numStates; i++) { 588 const char* desc = map->GetNthStateDesc(i); 589 590 if (desc) { 591 if (desc[0] == letter) { 592 curState = i; 593 break; 594 } 595 } else { 596 puts(B_TRANSLATE( 597 "Error, NULL state description?")); 598 } 599 } 600 } 601 } 602 fMetaCellStateIndex[whichColumn] = curState; 603 604 if (curState != origState) 605 result = true; 606 } 607 608 SetField(new BStringField(GetCellText(whichColumn)), whichColumn); 609 610 return result; 611 } 612 613 614 /*static*/ void 615 ShortcutsSpec::_InitModifierNames() 616 { 617 sShiftName = B_TRANSLATE_COMMENT("Shift", 618 "Name for modifier on keyboard"); 619 sControlName = B_TRANSLATE_COMMENT("Control", 620 "Name for modifier on keyboard"); 621 // TODO: Wrapping in __INTEL__ define probably won't work to extract catkeys? 622 #if __INTEL__ 623 sOptionName = B_TRANSLATE_COMMENT("Option", 624 "Name for modifier on keyboard"); 625 sCommandName = B_TRANSLATE_COMMENT("Alt", 626 "Name for modifier on keyboard"); 627 #else 628 sOptionName = B_TRANSLATE_COMMENT("Option", 629 "Name for modifier on keyboard"); 630 sCommandName = B_TRANSLATE_COMMENT("Command", 631 "Name for modifier on keyboard"); 632 #endif 633 } 634