wxMimeTypesManager now supports creating associations as well as querying
[wxWidgets.git] / src / msw / mimetype.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 // Name: msw/mimetype.cpp
3 // Purpose: classes and functions to manage MIME types
4 // Author: Vadim Zeitlin
5 // Modified by:
6 // Created: 23.09.98
7 // RCS-ID: $Id$
8 // Copyright: (c) 1998 Vadim Zeitlin <zeitlin@dptmaths.ens-cachan.fr>
9 // Licence: wxWindows license (part of wxExtra library)
10 /////////////////////////////////////////////////////////////////////////////
11
12 #ifdef __GNUG__
13 #pragma implementation "mimetype.h"
14 #endif
15
16 // for compilers that support precompilation, includes "wx.h".
17 #include "wx/wxprec.h"
18
19 #ifdef __BORLANDC__
20 #pragma hdrstop
21 #endif
22
23 // Doesn't compile in WIN16 mode
24 #ifndef __WIN16__
25
26 #ifndef WX_PRECOMP
27 #include "wx/string.h"
28 #if wxUSE_GUI
29 #include "wx/icon.h"
30 #endif
31 #endif //WX_PRECOMP
32
33 #include "wx/log.h"
34 #include "wx/file.h"
35 #include "wx/intl.h"
36 #include "wx/dynarray.h"
37 #include "wx/confbase.h"
38
39 #ifdef __WXMSW__
40 #include "wx/msw/registry.h"
41 #include "windows.h"
42 #endif // OS
43
44 #include "wx/msw/mimetype.h"
45
46 // other standard headers
47 #include <ctype.h>
48
49 // in case we're compiling in non-GUI mode
50 class WXDLLEXPORT wxIcon;
51
52 // These classes use Windows registry to retrieve the required information.
53 //
54 // Keys used (not all of them are documented, so it might actually stop working
55 // in future versions of Windows...):
56 // 1. "HKCR\MIME\Database\Content Type" contains subkeys for all known MIME
57 // types, each key has a string value "Extension" which gives (dot preceded)
58 // extension for the files of this MIME type.
59 //
60 // 2. "HKCR\.ext" contains
61 // a) unnamed value containing the "filetype"
62 // b) value "Content Type" containing the MIME type
63 //
64 // 3. "HKCR\filetype" contains
65 // a) unnamed value containing the description
66 // b) subkey "DefaultIcon" with single unnamed value giving the icon index in
67 // an icon file
68 // c) shell\open\command and shell\open\print subkeys containing the commands
69 // to open/print the file (the positional parameters are introduced by %1,
70 // %2, ... in these strings, we change them to %s ourselves)
71
72 // although I don't know of any official documentation which mentions this
73 // location, uses it, so it isn't likely to change
74 static const wxChar *MIME_DATABASE_KEY = wxT("MIME\\Database\\Content Type\\");
75
76 void wxFileTypeImpl::Init(const wxString& strFileType, const wxString& ext)
77 {
78 // VZ: does it? (FIXME)
79 wxCHECK_RET( !ext.IsEmpty(), _T("needs an extension") );
80
81 if ( ext[0u] != wxT('.') ) {
82 m_ext = wxT('.');
83 }
84 m_ext << ext;
85
86 m_strFileType = strFileType;
87 if ( !strFileType ) {
88 m_strFileType = m_ext.AfterFirst('.') + "_auto_file";
89 }
90 }
91
92 wxString wxFileTypeImpl::GetVerbPath(const wxString& verb) const
93 {
94 wxString path;
95 path << m_strFileType << _T("\\shell\\") << verb << _T("\\command");
96 return path;
97 }
98
99 size_t wxFileTypeImpl::GetAllCommands(wxArrayString *verbs,
100 wxArrayString *commands,
101 const wxFileType::MessageParameters& params) const
102 {
103 wxCHECK_MSG( !m_ext.IsEmpty(), 0, _T("GetAllCommands() needs an extension") );
104
105 if ( m_strFileType.IsEmpty() )
106 {
107 // get it from the registry
108 wxFileTypeImpl *self = wxConstCast(this, wxFileTypeImpl);
109 wxRegKey rkey(wxRegKey::HKCR, m_ext);
110 if ( !rkey.Exists() || !rkey.QueryValue(_T(""), self->m_strFileType) )
111 {
112 wxLogDebug(_T("Can't get the filetype for extension '%s'."),
113 m_ext.c_str());
114
115 return 0;
116 }
117 }
118
119 // enum all subkeys of HKCR\filetype\shell
120 size_t count = 0;
121 wxRegKey rkey(wxRegKey::HKCR, m_strFileType + _T("\\shell"));
122 long dummy;
123 wxString verb;
124 bool ok = rkey.GetFirstKey(verb, dummy);
125 while ( ok )
126 {
127 wxString command = wxFileType::ExpandCommand(GetCommand(verb), params);
128
129 // we want the open bverb to eb always the first
130
131 if ( verb.CmpNoCase(_T("open")) == 0 )
132 {
133 if ( verbs )
134 verbs->Insert(verb, 0);
135 if ( commands )
136 commands->Insert(command, 0);
137 }
138 else // anything else than "open"
139 {
140 if ( verbs )
141 verbs->Add(verb);
142 if ( commands )
143 commands->Add(command);
144 }
145
146 ok = rkey.GetNextKey(verb, dummy);
147 }
148
149 return count;
150 }
151
152 // ----------------------------------------------------------------------------
153 // modify the registry database
154 // ----------------------------------------------------------------------------
155
156 bool wxFileTypeImpl::EnsureExtKeyExists()
157 {
158 wxRegKey rkey(wxRegKey::HKCR, m_ext);
159 if ( !rkey.Exists() )
160 {
161 if ( !rkey.Create() || !rkey.SetValue(_T(""), m_strFileType) )
162 {
163 wxLogError(_("Failed to create registry entry for '%s' files."),
164 m_ext.c_str());
165 return FALSE;
166 }
167 }
168
169 return TRUE;
170 }
171
172 bool wxFileTypeImpl::SetCommand(const wxString& cmd,
173 const wxString& verb,
174 bool overwriteprompt)
175 {
176 wxCHECK_MSG( !m_ext.IsEmpty() && !verb.IsEmpty(), FALSE,
177 _T("SetCommand() needs an extension and a verb") );
178
179 if ( !EnsureExtKeyExists() )
180 return FALSE;
181
182 wxRegKey rkey(wxRegKey::HKCR, GetVerbPath(verb));
183
184 if ( rkey.Exists() && overwriteprompt )
185 {
186 #if wxUSE_GUI
187 wxString old;
188 rkey.QueryValue(wxT(""), old);
189 if ( wxMessageBox
190 (
191 wxString::Format(
192 _("Do you want to overwrite the command used to %s "
193 "files with extension \"%s\" (current value is '%s', "
194 "new value is '%s')?"),
195 verb.c_str(),
196 m_ext.c_str(),
197 old.c_str(),
198 cmd.c_str()),
199 _("Confirm registry update"),
200 wxYES_NO | wxICON_QUESTION
201 ) != wxYES )
202 #endif // wxUSE_GUI
203 {
204 // cancelled by user
205 return FALSE;
206 }
207 }
208
209 // TODO:
210 // 1. translate '%s' to '%1' instead of always adding it
211 // 2. create DDEExec value if needed (undo GetCommand)
212 return rkey.Create() && rkey.SetValue(_T(""), cmd + _T(" \"%1\"") );
213 }
214
215 bool wxFileTypeImpl::SetMimeType(const wxString& mimeTypeOrig)
216 {
217 wxCHECK_MSG( !m_ext.IsEmpty(), FALSE, _T("SetMimeType() needs extension") );
218
219 if ( !EnsureExtKeyExists() )
220 return FALSE;
221
222 // VZ: is this really useful? (FIXME)
223 wxString mimeType;
224 if ( !mimeTypeOrig )
225 {
226 // make up a default value for it
227 wxString cmd;
228 wxSplitPath(GetCommand(_T("open")), NULL, &cmd, NULL);
229 mimeType << _T("application/x-") << cmd;
230 }
231 else
232 {
233 mimeType = mimeTypeOrig;
234 }
235
236 wxRegKey rkey(wxRegKey::HKCR, m_ext);
237 return rkey.Create() && rkey.SetValue(_T("Content Type"), mimeType);
238 }
239
240 bool wxFileTypeImpl::SetDefaultIcon(const wxString& cmd, int index)
241 {
242 wxCHECK_MSG( !m_ext.IsEmpty(), FALSE, _T("SetMimeType() needs extension") );
243 wxCHECK_MSG( wxFileExists(cmd), FALSE, _T("Icon file not found.") );
244
245 if ( !EnsureExtKeyExists() )
246 return FALSE;
247
248 wxRegKey rkey(wxRegKey::HKCR, m_strFileType + _T("\\DefaultIcon"));
249
250 return rkey.Create() &&
251 rkey.SetValue(_T(""),
252 wxString::Format(_T("%s,%d"), cmd.c_str(), index));
253 }
254
255 // ----------------------------------------------------------------------------
256 // remove file association
257 // ----------------------------------------------------------------------------
258
259 bool wxFileTypeImpl::RemoveCommand(const wxString& verb)
260 {
261 wxCHECK_MSG( !m_ext.IsEmpty() && !verb.IsEmpty(), FALSE,
262 _T("RemoveCommand() needs an extension and a verb") );
263
264 wxString sKey = m_strFileType;
265 wxRegKey rkey(wxRegKey::HKCR, GetVerbPath(verb));
266
267 // if the key already doesn't exist, it's a success
268 return !rkey.Exists() || rkey.DeleteSelf();
269 }
270
271 bool wxFileTypeImpl::RemoveMimeType()
272 {
273 wxCHECK_MSG( !m_ext.IsEmpty(), FALSE, _T("RemoveMimeType() needs extension") );
274
275 wxRegKey rkey(wxRegKey::HKCR, m_ext);
276 return !rkey.Exists() || rkey.DeleteSelf();
277 }
278
279 bool wxFileTypeImpl::RemoveDefaultIcon()
280 {
281 wxCHECK_MSG( !m_ext.IsEmpty(), FALSE,
282 _T("RemoveDefaultIcon() needs extension") );
283
284 wxRegKey rkey (wxRegKey::HKCR, m_strFileType + _T("\\DefaultIcon"));
285 return !rkey.Exists() || rkey.DeleteSelf();
286 }
287
288 wxString wxFileTypeImpl::GetCommand(const wxChar *verb) const
289 {
290 // suppress possible error messages
291 wxLogNull nolog;
292 wxString strKey;
293
294 if ( wxRegKey(wxRegKey::HKCR, m_ext + _T("\\shell")).Exists() )
295 strKey = m_ext;
296 if ( wxRegKey(wxRegKey::HKCR, m_strFileType + _T("\\shell")).Exists() )
297 strKey = m_strFileType;
298
299 if ( !strKey )
300 {
301 // no info
302 return wxEmptyString;
303 }
304
305 strKey << wxT("\\shell\\") << verb;
306 wxRegKey key(wxRegKey::HKCR, strKey + _T("\\command"));
307 wxString command;
308 if ( key.Open() ) {
309 // it's the default value of the key
310 if ( key.QueryValue(wxT(""), command) ) {
311 // transform it from '%1' to '%s' style format string (now also
312 // test for %L - apparently MS started using it as well for the
313 // same purpose)
314
315 // NB: we don't make any attempt to verify that the string is valid,
316 // i.e. doesn't contain %2, or second %1 or .... But we do make
317 // sure that we return a string with _exactly_ one '%s'!
318 bool foundFilename = FALSE;
319 size_t len = command.Len();
320 for ( size_t n = 0; (n < len) && !foundFilename; n++ ) {
321 if ( command[n] == wxT('%') &&
322 (n + 1 < len) &&
323 (command[n + 1] == wxT('1') ||
324 command[n + 1] == wxT('L')) ) {
325 // replace it with '%s'
326 command[n + 1] = wxT('s');
327
328 foundFilename = TRUE;
329 }
330 }
331
332 #if wxUSE_IPC
333 // look whether we must issue some DDE requests to the application
334 // (and not just launch it)
335 strKey += _T("\\DDEExec");
336 wxRegKey keyDDE(wxRegKey::HKCR, strKey);
337 if ( keyDDE.Open() ) {
338 wxString ddeCommand, ddeServer, ddeTopic;
339 keyDDE.QueryValue(_T(""), ddeCommand);
340 ddeCommand.Replace(_T("%1"), _T("%s"));
341
342 wxRegKey(wxRegKey::HKCR, strKey + _T("\\Application")).
343 QueryValue(_T(""), ddeServer);
344 wxRegKey(wxRegKey::HKCR, strKey + _T("\\Topic")).
345 QueryValue(_T(""), ddeTopic);
346
347 // HACK: we use a special feature of wxExecute which exists
348 // just because we need it here: it will establish DDE
349 // conversation with the program it just launched
350 command.Prepend(_T("WX_DDE#"));
351 command << _T('#') << ddeServer
352 << _T('#') << ddeTopic
353 << _T('#') << ddeCommand;
354 }
355 else
356 #endif // wxUSE_IPC
357 if ( !foundFilename ) {
358 // we didn't find any '%1' - the application doesn't know which
359 // file to open (note that we only do it if there is no DDEExec
360 // subkey)
361 //
362 // HACK: append the filename at the end, hope that it will do
363 command << wxT(" %s");
364 }
365 }
366 }
367 //else: no such file type or no value, will return empty string
368
369 return command;
370 }
371
372 bool
373 wxFileTypeImpl::GetOpenCommand(wxString *openCmd,
374 const wxFileType::MessageParameters& params)
375 const
376 {
377 wxString cmd;
378 if ( m_info ) {
379 cmd = m_info->GetOpenCommand();
380 }
381 else {
382 cmd = GetCommand(wxT("open"));
383 }
384
385 *openCmd = wxFileType::ExpandCommand(cmd, params);
386
387 return !openCmd->IsEmpty();
388 }
389
390 bool
391 wxFileTypeImpl::GetPrintCommand(wxString *printCmd,
392 const wxFileType::MessageParameters& params)
393 const
394 {
395 wxString cmd;
396 if ( m_info ) {
397 cmd = m_info->GetPrintCommand();
398 }
399 else {
400 cmd = GetCommand(wxT("print"));
401 }
402
403 *printCmd = wxFileType::ExpandCommand(cmd, params);
404
405 return !printCmd->IsEmpty();
406 }
407
408 // TODO this function is half implemented
409 bool wxFileTypeImpl::GetExtensions(wxArrayString& extensions)
410 {
411 if ( m_info ) {
412 extensions = m_info->GetExtensions();
413
414 return TRUE;
415 }
416 else if ( m_ext.IsEmpty() ) {
417 // the only way to get the list of extensions from the file type is to
418 // scan through all extensions in the registry - too slow...
419 return FALSE;
420 }
421 else {
422 extensions.Empty();
423 extensions.Add(m_ext);
424
425 // it's a lie too, we don't return _all_ extensions...
426 return TRUE;
427 }
428 }
429
430 bool wxFileTypeImpl::GetMimeType(wxString *mimeType) const
431 {
432 if ( m_info ) {
433 // we already have it
434 *mimeType = m_info->GetMimeType();
435
436 return TRUE;
437 }
438
439 // suppress possible error messages
440 wxLogNull nolog;
441 wxRegKey key(wxRegKey::HKCR, m_ext);
442
443 return key.Open() && key.QueryValue(wxT("Content Type"), *mimeType);
444 }
445
446 bool wxFileTypeImpl::GetMimeTypes(wxArrayString& mimeTypes) const
447 {
448 wxString s;
449
450 if ( !GetMimeType(&s) )
451 {
452 return FALSE;
453 }
454
455 mimeTypes.Clear();
456 mimeTypes.Add(s);
457 return TRUE;
458 }
459
460
461 bool wxFileTypeImpl::GetIcon(wxIcon *icon,
462 wxString *iconFile,
463 int *iconIndex) const
464 {
465 #if wxUSE_GUI
466 if ( m_info ) {
467 // we don't have icons in the fallback resources
468 return FALSE;
469 }
470
471 wxString strIconKey;
472 strIconKey << m_strFileType << wxT("\\DefaultIcon");
473
474 // suppress possible error messages
475 wxLogNull nolog;
476 wxRegKey key(wxRegKey::HKCR, strIconKey);
477
478 if ( key.Open() ) {
479 wxString strIcon;
480 // it's the default value of the key
481 if ( key.QueryValue(wxT(""), strIcon) ) {
482 // the format is the following: <full path to file>, <icon index>
483 // NB: icon index may be negative as well as positive and the full
484 // path may contain the environment variables inside '%'
485 wxString strFullPath = strIcon.BeforeLast(wxT(',')),
486 strIndex = strIcon.AfterLast(wxT(','));
487
488 // index may be omitted, in which case BeforeLast(',') is empty and
489 // AfterLast(',') is the whole string
490 if ( strFullPath.IsEmpty() ) {
491 strFullPath = strIndex;
492 strIndex = wxT("0");
493 }
494
495 wxString strExpPath = wxExpandEnvVars(strFullPath);
496 int nIndex = wxAtoi(strIndex) - 1 ; //bug here we need C based counting!!
497
498 HICON hIcon = ExtractIcon(GetModuleHandle(NULL), strExpPath, nIndex);
499 switch ( (int)hIcon ) {
500 case 0: // means no icons were found
501 case 1: // means no such file or it wasn't a DLL/EXE/OCX/ICO/...
502 wxLogDebug(wxT("incorrect registry entry '%s': no such icon."),
503 key.GetName().c_str());
504 break;
505
506 default:
507 icon->SetHICON((WXHICON)hIcon);
508 if ( iconIndex )
509 *iconIndex = nIndex;
510 if ( iconFile )
511 *iconFile = strFullPath;
512 return TRUE;
513 }
514 }
515 }
516
517 // no such file type or no value or incorrect icon entry
518 #endif // wxUSE_GUI
519
520 return FALSE;
521 }
522
523 bool wxFileTypeImpl::GetDescription(wxString *desc) const
524 {
525 if ( m_info ) {
526 // we already have it
527 *desc = m_info->GetDescription();
528
529 return TRUE;
530 }
531
532 // suppress possible error messages
533 wxLogNull nolog;
534 wxRegKey key(wxRegKey::HKCR, m_strFileType);
535
536 if ( key.Open() ) {
537 // it's the default value of the key
538 if ( key.QueryValue(wxT(""), *desc) ) {
539 return TRUE;
540 }
541 }
542
543 return FALSE;
544 }
545
546 // helper function
547 wxFileType *
548 wxMimeTypesManagerImpl::CreateFileType(const wxString& filetype, const wxString& ext)
549 {
550 wxFileType *fileType = new wxFileType;
551 fileType->m_impl->Init(filetype, ext);
552 return fileType;
553 }
554
555 // extension -> file type
556 wxFileType *
557 wxMimeTypesManagerImpl::GetFileTypeFromExtension(const wxString& ext)
558 {
559 // add the leading point if necessary
560 wxString str;
561 if ( ext[0u] != wxT('.') ) {
562 str = wxT('.');
563 }
564 str << ext;
565
566 // suppress possible error messages
567 wxLogNull nolog;
568
569 bool knownExtension = FALSE;
570
571 wxString strFileType;
572 wxRegKey key(wxRegKey::HKCR, str);
573 if ( key.Open() ) {
574 // it's the default value of the key
575 if ( key.QueryValue(wxT(""), strFileType) ) {
576 // create the new wxFileType object
577 return CreateFileType(strFileType, ext);
578 }
579 else {
580 // this extension doesn't have a filetype, but it's known to the
581 // system and may be has some other useful keys (open command or
582 // content-type), so still return a file type object for it
583 knownExtension = TRUE;
584 }
585 }
586
587 // check the fallbacks
588 // TODO linear search is potentially slow, perhaps we should use a sorted
589 // array?
590 size_t count = m_fallbacks.GetCount();
591 for ( size_t n = 0; n < count; n++ ) {
592 if ( m_fallbacks[n].GetExtensions().Index(ext) != wxNOT_FOUND ) {
593 wxFileType *fileType = new wxFileType;
594 fileType->m_impl->Init(m_fallbacks[n]);
595
596 return fileType;
597 }
598 }
599
600 if ( !knownExtension )
601 {
602 // unknown extension
603 return NULL;
604 }
605
606 return CreateFileType(wxEmptyString, ext);
607 }
608
609 wxFileType *
610 wxMimeTypesManagerImpl::GetOrAllocateFileTypeFromExtension(const wxString& ext)
611 {
612 wxFileType *fileType = GetFileTypeFromExtension(ext);
613 if ( !fileType )
614 {
615 fileType = CreateFileType(wxEmptyString, ext);
616 }
617
618 return fileType;
619 }
620
621
622 // MIME type -> extension -> file type
623 wxFileType *
624 wxMimeTypesManagerImpl::GetFileTypeFromMimeType(const wxString& mimeType)
625 {
626 wxString strKey = MIME_DATABASE_KEY;
627 strKey << mimeType;
628
629 // suppress possible error messages
630 wxLogNull nolog;
631
632 wxString ext;
633 wxRegKey key(wxRegKey::HKCR, strKey);
634 if ( key.Open() ) {
635 if ( key.QueryValue(wxT("Extension"), ext) ) {
636 return GetFileTypeFromExtension(ext);
637 }
638 }
639
640 // check the fallbacks
641 // TODO linear search is potentially slow, perhaps we should use a sorted
642 // array?
643 size_t count = m_fallbacks.GetCount();
644 for ( size_t n = 0; n < count; n++ ) {
645 if ( wxMimeTypesManager::IsOfType(mimeType,
646 m_fallbacks[n].GetMimeType()) ) {
647 wxFileType *fileType = new wxFileType;
648 fileType->m_impl->Init(m_fallbacks[n]);
649
650 return fileType;
651 }
652 }
653
654 // unknown MIME type
655 return NULL;
656 }
657
658 size_t wxMimeTypesManagerImpl::EnumAllFileTypes(wxArrayString& mimetypes)
659 {
660 // enumerate all keys under MIME_DATABASE_KEY
661 wxRegKey key(wxRegKey::HKCR, MIME_DATABASE_KEY);
662
663 wxString type;
664 long cookie;
665 bool cont = key.GetFirstKey(type, cookie);
666 while ( cont )
667 {
668 mimetypes.Add(type);
669
670 cont = key.GetNextKey(type, cookie);
671 }
672
673 return mimetypes.GetCount();
674 }
675
676 // ----------------------------------------------------------------------------
677 // create a new association
678 // ----------------------------------------------------------------------------
679
680 wxFileType *wxMimeTypesManager::Associate(const wxString& ext,
681 const wxString& mimetype,
682 const wxString& filetypeOrig,
683 const wxString& WXUNUSED(desc))
684 {
685 wxCHECK_MSG( !ext.empty(), NULL, _T("Associate() needs extension") );
686
687 wxString extWithDot;
688 if ( ext[0u] != _T('.') )
689 extWithDot = _T('.');
690 extWithDot += ext;
691
692 wxRegKey key(wxRegKey::HKCR, extWithDot);
693 wxFileType *ft = NULL;
694 if ( !key.Exists() )
695 {
696 wxString filetype;
697
698 // create the mapping from the extension to the filetype
699 bool ok = key.Create();
700 if ( ok )
701 {
702 if ( filetypeOrig.empty() )
703 {
704 // make it up from the extension
705 filetype << extWithDot.c_str() + 1 << _T("_auto_file");
706 }
707 else
708 {
709 // just use the provided one
710 filetype = filetypeOrig;
711 }
712
713 ok = key.SetValue(_T(""), filetype);
714 }
715
716 if ( ok && !mimetype.empty() )
717 {
718 // set the MIME type
719 ok = key.SetValue(_T("Content Type"), mimetype);
720
721 if ( ok )
722 {
723 // create the MIME key
724 wxString strKey = MIME_DATABASE_KEY;
725 strKey << mimetype;
726 wxRegKey keyMIME(wxRegKey::HKCR, strKey);
727 ok = keyMIME.Create();
728
729 if ( ok )
730 {
731 // and provide a back link to the extension
732 ok = keyMIME.SetValue(_T("Extension"), extWithDot);
733 }
734 }
735 }
736
737 if ( ok )
738 {
739 // create the filetype key itself (it will be empty for now, but
740 // SetCommand(), SetDefaultIcon() &c will use it later)
741 wxRegKey keyFT(wxRegKey::HKCR, filetype);
742 ok = keyFT.Create();
743 }
744
745 if ( ok )
746 {
747 // ok, we've created everything correctly
748 ft = m_impl->CreateFileType(filetype, extWithDot);
749 }
750 else
751 {
752 // one of the registry operations failed
753 wxLogError(_("Failed to register extension '%s'."), ext.c_str());
754 }
755 }
756 else // key already exists
757 {
758 // FIXME we probably should return an existing file type then?
759 }
760
761 return ft;
762 }
763
764 #endif
765 // __WIN16__