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