Ticket #12307: edithandler.cpp

File edithandler.cpp, 54.0 KB (added by Marco Meyer, 4 years ago)

Including some cleaning and comments about the procedure

Line 
1#include "filezilla.h"
2#include "conditionaldialog.h"
3#include "dialogex.h"
4#include "edithandler.h"
5#include "filezillaapp.h"
6#include "file_utils.h"
7#include "Options.h"
8#include "queue.h"
9#include "textctrlex.h"
10#include "window_state_manager.h"
11
12#include <libfilezilla/file.hpp>
13#include <libfilezilla/local_filesys.hpp>
14#include <libfilezilla/process.hpp>
15
16#include <wx/filedlg.h>
17#include <wx/hyperlink.h>
18#include <wx/statline.h>
19
20#include <stdio.h>
21#include <string.h>
22#include <stdlib.h>
23//-------------
24
25wxDECLARE_EVENT(fzEDIT_CHANGEDFILE, wxCommandEvent);
26wxDEFINE_EVENT(fzEDIT_CHANGEDFILE, wxCommandEvent);
27
28BEGIN_EVENT_TABLE(CEditHandler, wxEvtHandler)
29EVT_TIMER(wxID_ANY, CEditHandler::OnTimerEvent)
30EVT_COMMAND(wxID_ANY, fzEDIT_CHANGEDFILE, CEditHandler::OnChangedFileEvent)
31END_EVENT_TABLE()
32
33Associations LoadAssociations()
34{
35 Associations ret;
36
37 std::wstring const raw_assocs = COptions::Get()->get_string(OPTION_EDIT_CUSTOMASSOCIATIONS);
38 auto assocs = fz::strtok_view(raw_assocs, L"\r\n", true);
39
40 for (std::wstring_view assoc : assocs) {
41 std::optional<std::wstring> ext = UnquoteFirst(assoc);
42 if (!ext || ext->empty()) {
43 continue;
44 }
45
46 auto unquoted = UnquoteCommand(assoc);
47 if (!unquoted.empty() && !unquoted[0].empty()) {
48 ret.emplace(std::move(*ext), std::move(unquoted));
49 }
50 }
51
52 return ret;
53
54}
55
56void SaveAssociations(Associations const& assocs)
57{
58 std::wstring quoted;
59 for (auto const& assoc : assocs) {
60 if (!quoted.empty()) {
61 quoted += '\n';
62 }
63
64 if (assoc.first.find_first_of(L" \t'\"") != std::wstring::npos) {
65 quoted += '"';
66 quoted += fz::replaced_substrings(assoc.first, L"\"", L"\"\"");
67 quoted += '"';
68 }
69 else {
70 quoted += assoc.first;
71 }
72 quoted += ' ';
73 quoted += QuoteCommand(assoc.second);
74 }
75 COptions::Get()->set(OPTION_EDIT_CUSTOMASSOCIATIONS, quoted);
76}
77
78CEditHandler* CEditHandler::m_pEditHandler = 0;
79
80CEditHandler::CEditHandler()
81{
82 m_pQueue = 0;
83
84 m_timer.SetOwner(this);
85 m_busyTimer.SetOwner(this);
86
87#ifdef __WXMSW__
88 m_lockfile_handle = INVALID_HANDLE_VALUE;
89#else
90 m_lockfile_descriptor = -1;
91#endif
92}
93
94CEditHandler* CEditHandler::Create()
95{
96 if (!m_pEditHandler) {
97 m_pEditHandler = new CEditHandler();
98 }
99
100 return m_pEditHandler;
101}
102
103CEditHandler* CEditHandler::Get()
104{
105 return m_pEditHandler;
106}
107
108void CEditHandler::RemoveTemporaryFiles(std::wstring const& temp)
109{
110 wxDir dir(temp);
111 if (!dir.IsOpened()) {
112 return;
113 }
114
115 wxString file;
116 if (!dir.GetFirst(&file, _T("fz3temp-*"), wxDIR_DIRS)) {
117 return;
118 }
119
120 wxChar const& sep = wxFileName::GetPathSeparator();
121 do {
122 if (!m_localDir.empty() && temp + file + sep == m_localDir) {
123 // Don't delete own working directory
124 continue;
125 }
126
127 RemoveTemporaryFilesInSpecificDir((temp + file + sep).ToStdWstring());
128 } while (dir.GetNext(&file));
129}
130
131void CEditHandler::RemoveTemporaryFilesInSpecificDir(std::wstring const& temp)
132{
133 std::wstring const lockfile = temp + L"fz3temp-lockfile";
134 if (wxFileName::FileExists(lockfile)) {
135#ifndef __WXMSW__
136 int fd = open(fz::to_string(lockfile).c_str(), O_RDWR | O_CLOEXEC, 0);
137 if (fd >= 0) {
138 // Try to lock 1 byte region in the lockfile. m_type specifies the byte to lock.
139 struct flock f = {};
140 f.l_type = F_WRLCK;
141 f.l_whence = SEEK_SET;
142 f.l_start = 0;
143 f.l_len = 1;
144 f.l_pid = getpid();
145 if (fcntl(fd, F_SETLK, &f)) {
146 // In use by other process
147 close(fd);
148 return;
149 }
150 close(fd);
151 }
152#endif
153 fz::remove_file(fz::to_native(lockfile));
154
155 if (wxFileName::FileExists(lockfile)) {
156 return;
157 }
158 }
159
160 wxLogNull log;
161
162 {
163 wxString file;
164 wxDir dir(temp);
165 bool res;
166 for ((res = dir.GetFirst(&file, _T(""), wxDIR_FILES)); res; res = dir.GetNext(&file)) {
167 wxRemoveFile(temp + file);
168 }
169 }
170
171 wxRmdir(temp);
172
173}
174
175std::wstring CEditHandler::GetLocalDirectory()
176{
177 if (!m_localDir.empty()) {
178 return m_localDir;
179 }
180
181 wxFileName tmpdir(wxFileName::GetTempDir(), _T(""));
182 // Need to call GetLongPath on MSW, GetTempDir can return short path
183 // which will cause problems when calculating maximum allowed file
184 // length
185 wxString dir = tmpdir.GetLongPath();
186 if (dir.empty() || !wxFileName::DirExists(dir)) {
187 return std::wstring();
188 }
189
190 if (dir.Last() != wxFileName::GetPathSeparator()) {
191 dir += wxFileName::GetPathSeparator();
192 }
193
194 // On POSIX, the permissions of the created directory (700) ensure
195 // that this is a safe operation.
196 // On Windows, the user's profile directory and associated temp dir
197 // already has the correct permissions which get inherited.
198 int i = 1;
199 do {
200 wxString newDir = dir + wxString::Format(_T("fz3temp-%d"), ++i);
201 if (wxFileName::FileExists(newDir) || wxFileName::DirExists(newDir)) {
202 continue;
203 }
204
205 if (!wxMkdir(newDir, 0700)) {
206 return std::wstring();
207 }
208
209 m_localDir = (newDir + wxFileName::GetPathSeparator()).ToStdWstring();
210 break;
211 } while (true);
212
213 // Defer deleting stale directories until after having created our own
214 // working directory.
215 // This avoids some strange errors where freshly deleted directories
216 // cannot be instantly recreated.
217 RemoveTemporaryFiles(dir.ToStdWstring());
218
219#ifdef __WXMSW__
220 m_lockfile_handle = ::CreateFile((m_localDir + L"fz3temp-lockfile").c_str(), GENERIC_WRITE, 0, 0, CREATE_NEW, FILE_ATTRIBUTE_TEMPORARY, 0);
221 if (m_lockfile_handle == INVALID_HANDLE_VALUE) {
222 wxRmdir(m_localDir);
223 m_localDir.clear();
224 }
225#else
226 auto file = fz::to_native(m_localDir) + "fz3temp-lockfile";
227 m_lockfile_descriptor = open(file.c_str(), O_CREAT | O_RDWR | O_CLOEXEC, 0600);
228 if (m_lockfile_descriptor >= 0) {
229 // Lock 1 byte region in the lockfile.
230 struct flock f = {};
231 f.l_type = F_WRLCK;
232 f.l_whence = SEEK_SET;
233 f.l_start = 0;
234 f.l_len = 1;
235 f.l_pid = getpid();
236 fcntl(m_lockfile_descriptor, F_SETLKW, &f);
237 }
238#endif
239
240 return m_localDir;
241}
242
243void CEditHandler::Release()
244{
245 if (m_timer.IsRunning()) {
246 m_timer.Stop();
247 }
248 if (m_busyTimer.IsRunning()) {
249 m_busyTimer.Stop();
250 }
251
252 if (!m_localDir.empty()) {
253#ifdef __WXMSW__
254 if (m_lockfile_handle != INVALID_HANDLE_VALUE) {
255 CloseHandle(m_lockfile_handle);
256 }
257 wxRemoveFile(m_localDir + _T("fz3temp-lockfile"));
258#else
259 wxRemoveFile(m_localDir + _T("fz3temp-lockfile"));
260 if (m_lockfile_descriptor >= 0) {
261 close(m_lockfile_descriptor);
262 }
263#endif
264
265 wxLogNull log;
266 wxRemoveFile(m_localDir + _T("empty_file_yq744zm"));
267 RemoveAll(true);
268 RemoveTemporaryFilesInSpecificDir(m_localDir);
269 }
270
271 m_pEditHandler = 0;
272 delete this;
273}
274
275CEditHandler::fileState CEditHandler::GetFileState(std::wstring const& fileName) const
276{
277 std::list<t_fileData>::const_iterator iter = GetFile(fileName);
278 if (iter == m_fileDataList[local].end()) {
279 return unknown;
280 }
281
282 return iter->state;
283}
284
285CEditHandler::fileState CEditHandler::GetFileState(std::wstring const& fileName, CServerPath const& remotePath, Site const& site) const
286{
287 std::list<t_fileData>::const_iterator iter = GetFile(fileName, remotePath, site);
288 if (iter == m_fileDataList[remote].end()) {
289 return unknown;
290 }
291
292 return iter->state;
293}
294
295int CEditHandler::GetFileCount(CEditHandler::fileType type, CEditHandler::fileState state, Site const& site) const
296{
297 int count = 0;
298 if (state == unknown) {
299 wxASSERT(!site);
300 if (type != remote) {
301 count += m_fileDataList[local].size();
302 }
303 if (type != local) {
304 count += m_fileDataList[remote].size();
305 }
306 }
307 else {
308 auto f = [state, &site](decltype(m_fileDataList[0]) & items) {
309 int cnt = 0;
310 for (auto const& data : items) {
311 if (data.state != state) {
312 continue;
313 }
314
315 if (!site || data.site == site) {
316 ++cnt;
317 }
318 }
319 return cnt;
320 };
321 if (type != remote) {
322 count += f(m_fileDataList[local]);
323 }
324 if (type != local) {
325 count += f(m_fileDataList[remote]);
326 }
327 }
328
329 return count;
330}
331
332bool CEditHandler::AddFile(CEditHandler::fileType type, std::wstring const& localFile, std::wstring const& remoteFile, CServerPath const& remotePath, Site const& site, int64_t size)
333{
334 wxASSERT(type != none);
335
336 fileState state;
337 if (type == local) {
338 state = GetFileState(localFile);
339 }
340 else {
341 state = GetFileState(remoteFile, remotePath, site);
342 }
343
344 // It should still be unknown, but due to having displayed dialogs with event loops, something else might have happened so check again just in case.
345 if (state != unknown) {
346 wxBell();
347 return false;
348 }
349
350 t_fileData data;
351 if (type == remote) {
352 data.state = download;
353 }
354 else {
355 data.state = edit;
356 }
357 data.localFile = localFile;
358 data.remoteFile = remoteFile;
359 data.remotePath = remotePath;
360 data.site = site;
361
362
363 if (type == local) {
364 bool const launched = LaunchEditor(local, data);
365
366 if (launched && COptions::Get()->get_int(OPTION_EDIT_TRACK_LOCAL)) {
367 m_fileDataList[type].emplace_back(std::move(data));
368 }
369 if (!launched) {
370 wxMessageBoxEx(wxString::Format(_("The file '%s' could not be opened:\nThe associated command failed"), localFile), _("Opening failed"), wxICON_EXCLAMATION);
371 }
372 return launched;
373 }
374 else {
375 m_fileDataList[type].emplace_back(std::move(data));
376
377 std::wstring localFileName;
378 CLocalPath localPath(localFile, &localFileName);
379 if (localFileName == remoteFile) {
380 localFileName.clear();
381 }
382 m_pQueue->QueueFile(false, true, remoteFile, localFileName, localPath, remotePath, site, size, type, QueuePriority::high);
383 m_pQueue->QueueFile_Finish(true);
384 }
385
386 return true;
387}
388
389bool CEditHandler::Remove(std::wstring const& fileName)
390{
391 std::list<t_fileData>::iterator iter = GetFile(fileName);
392 if (iter == m_fileDataList[local].end()) {
393 return true;
394 }
395
396 wxASSERT(iter->state != upload && iter->state != upload_and_remove);
397 if (iter->state == upload || iter->state == upload_and_remove) {
398 return false;
399 }
400
401 m_fileDataList[local].erase(iter);
402
403 return true;
404}
405
406bool CEditHandler::Remove(std::wstring const& fileName, CServerPath const& remotePath, Site const& site)
407{
408 std::list<t_fileData>::iterator iter = GetFile(fileName, remotePath, site);
409 if (iter == m_fileDataList[remote].end()) {
410 return true;
411 }
412
413 wxASSERT(iter->state != download && iter->state != upload && iter->state != upload_and_remove);
414 if (iter->state == download || iter->state == upload || iter->state == upload_and_remove) {
415 return false;
416 }
417
418 if (wxFileName::FileExists(iter->localFile)) {
419 if (!wxRemoveFile(iter->localFile)) {
420 iter->state = removing;
421 return false;
422 }
423 }
424
425 m_fileDataList[remote].erase(iter);
426
427 return true;
428}
429
430bool CEditHandler::RemoveAll(bool force)
431{
432 std::list<t_fileData> keep;
433
434 for (std::list<t_fileData>::iterator iter = m_fileDataList[remote].begin(); iter != m_fileDataList[remote].end(); ++iter) {
435 if (!force && (iter->state == download || iter->state == upload || iter->state == upload_and_remove)) {
436 keep.push_back(*iter);
437 continue;
438 }
439
440 if (wxFileName::FileExists(iter->localFile)) {
441 if (!wxRemoveFile(iter->localFile)) {
442 iter->state = removing;
443 keep.push_back(*iter);
444 continue;
445 }
446 }
447 }
448 m_fileDataList[remote].swap(keep);
449 keep.clear();
450
451 for (auto iter = m_fileDataList[local].begin(); iter != m_fileDataList[local].end(); ++iter) {
452 if (force) {
453 continue;
454 }
455
456 if (iter->state == upload || iter->state == upload_and_remove) {
457 keep.push_back(*iter);
458 continue;
459 }
460 }
461 m_fileDataList[local].swap(keep);
462
463 return m_fileDataList[local].empty() && m_fileDataList[remote].empty();
464}
465
466bool CEditHandler::RemoveAll(fileState state, Site const& site)
467{
468 // Others not implemented
469 wxASSERT(state == upload_and_remove_failed);
470 if (state != upload_and_remove_failed) {
471 return false;
472 }
473
474 std::list<t_fileData> keep;
475
476 for (auto iter = m_fileDataList[remote].begin(); iter != m_fileDataList[remote].end(); ++iter) {
477 if (iter->state != state) {
478 keep.push_back(*iter);
479 continue;
480 }
481
482 if (site && iter->site != site) {
483 keep.push_back(*iter);
484 continue;
485 }
486
487 if (wxFileName::FileExists(iter->localFile)) {
488 if (!wxRemoveFile(iter->localFile)) {
489 iter->state = removing;
490 keep.push_back(*iter);
491 continue;
492 }
493 }
494 }
495 m_fileDataList[remote].swap(keep);
496
497 return true;
498}
499
500std::list<CEditHandler::t_fileData>::iterator CEditHandler::GetFile(std::wstring const& fileName)
501{
502 std::list<t_fileData>::iterator iter;
503 for (iter = m_fileDataList[local].begin(); iter != m_fileDataList[local].end(); ++iter) {
504 if (iter->localFile == fileName) {
505 break;
506 }
507 }
508
509 return iter;
510}
511
512std::list<CEditHandler::t_fileData>::const_iterator CEditHandler::GetFile(std::wstring const& fileName) const
513{
514 std::list<t_fileData>::const_iterator iter;
515 for (iter = m_fileDataList[local].begin(); iter != m_fileDataList[local].end(); ++iter) {
516 if (iter->localFile == fileName) {
517 break;
518 }
519 }
520
521 return iter;
522}
523
524std::list<CEditHandler::t_fileData>::iterator CEditHandler::GetFile(std::wstring const& fileName, CServerPath const& remotePath, Site const& site)
525{
526 std::list<t_fileData>::iterator iter;
527 for (iter = m_fileDataList[remote].begin(); iter != m_fileDataList[remote].end(); ++iter) {
528 if (iter->remoteFile != fileName) {
529 continue;
530 }
531
532 if (iter->site != site) {
533 continue;
534 }
535
536 if (iter->remotePath != remotePath) {
537 continue;
538 }
539
540 return iter;
541 }
542
543 return iter;
544}
545
546std::list<CEditHandler::t_fileData>::const_iterator CEditHandler::GetFile(std::wstring const& fileName, CServerPath const& remotePath, Site const& site) const
547{
548 std::list<t_fileData>::const_iterator iter;
549 for (iter = m_fileDataList[remote].begin(); iter != m_fileDataList[remote].end(); ++iter) {
550 if (iter->remoteFile != fileName) {
551 continue;
552 }
553
554 if (iter->site != site) {
555 continue;
556 }
557
558 if (iter->remotePath != remotePath) {
559 continue;
560 }
561
562 return iter;
563 }
564
565 return iter;
566}
567
568void CEditHandler::FinishTransfer(bool, std::wstring const& fileName)
569{
570 auto iter = GetFile(fileName);
571 if (iter == m_fileDataList[local].end()) {
572 return;
573 }
574
575 wxASSERT(iter->state == upload || iter->state == upload_and_remove);
576
577 switch (iter->state)
578 {
579 case upload_and_remove:
580 m_fileDataList[local].erase(iter);
581 break;
582 case upload:
583 if (wxFileName::FileExists(fileName)) {
584 iter->state = edit;
585 }
586 else {
587 m_fileDataList[local].erase(iter);
588 }
589 break;
590 default:
591 return;
592 }
593
594 SetTimerState();
595}
596
597void CEditHandler::FinishTransfer(bool successful, std::wstring const& fileName, CServerPath const& remotePath, Site const& site)
598{
599 auto iter = GetFile(fileName, remotePath, site);
600 if (iter == m_fileDataList[remote].end()) {
601 return;
602 }
603
604 wxASSERT(iter->state == download || iter->state == upload || iter->state == upload_and_remove);
605
606 switch (iter->state)
607 {
608 case upload_and_remove:
609 if (successful) {
610 if (wxFileName::FileExists(iter->localFile) && !wxRemoveFile(iter->localFile)) {
611 iter->state = removing;
612 }
613 else {
614 m_fileDataList[remote].erase(iter);
615 }
616 }
617 else {
618 if (!wxFileName::FileExists(iter->localFile)) {
619 m_fileDataList[remote].erase(iter);
620 }
621 else {
622 iter->state = upload_and_remove_failed;
623 }
624 }
625 break;
626 case upload:
627 if (wxFileName::FileExists(iter->localFile)) {
628 iter->state = edit;
629 }
630 else {
631 m_fileDataList[remote].erase(iter);
632 }
633 break;
634 case download:
635 if (wxFileName::FileExists(iter->localFile)) {
636 iter->state = edit;
637 if (LaunchEditor(remote, *iter)) {
638 break;
639 }
640 }
641 if (wxFileName::FileExists(iter->localFile) && !wxRemoveFile(iter->localFile)) {
642 iter->state = removing;
643 }
644 else {
645 m_fileDataList[remote].erase(iter);
646 }
647 break;
648 default:
649 return;
650 }
651
652 SetTimerState();
653}
654
655bool CEditHandler::LaunchEditor(std::wstring const& file)
656{
657 auto iter = GetFile(file);
658 if (iter == m_fileDataList[local].end()) {
659 return false;
660 }
661
662 return LaunchEditor(local, *iter);
663}
664
665bool CEditHandler::LaunchEditor(std::wstring const& file, CServerPath const& remotePath, Site const& site)
666{
667 auto iter = GetFile(file, remotePath, site);
668 if (iter == m_fileDataList[remote].end()) {
669 return false;
670 }
671
672 return LaunchEditor(remote, *iter);
673}
674
675bool CEditHandler::LaunchEditor(CEditHandler::fileType type, t_fileData& data)
676{
677 wxASSERT(type != none);
678 wxASSERT(data.state == edit);
679
680 bool is_link;
681 if (fz::local_filesys::get_file_info(fz::to_native(data.localFile), is_link, 0, &data.modificationTime, 0) != fz::local_filesys::file) {
682 return false;
683 }
684
685 auto cmd_with_args = GetAssociation((type == local) ? data.localFile : data.remoteFile);
686 if (cmd_with_args.empty() || !ProgramExists(cmd_with_args.front())) {
687 return false;
688 }
689
690 return fz::spawn_detached_process(AssociationToCommand(cmd_with_args, data.localFile));
691}
692
693bool CEditHandler::SafeCopy(std::wstring const& from, std::wstring const& to)
694{
695 // Attempt to make a safe copy of the file being modified
696 std::ifstream ifile(from.data());
697
698 int iSuccess = 0;
699 for (int i = 0, N = 100; i <= N; i++)
700 {
701 // Backup file
702 std::ofstream ofile(to.data());
703 ofile << ifile.rdbuf();
704
705 // Reset cursor
706 ifile.clear();
707 ifile.seekg(0, std::ios::beg);
708
709 // Check if file was being written..
710 if ( SameFiles(to.data(), from.data()) ) iSuccess++;
711 else {
712 if(iSuccess) i = 0;
713 iSuccess = 0;
714 }
715
716 // Check if not modified during at least 1s
717 if(iSuccess > 10) return true;
718 SetTimerState(100);
719 }
720
721 //std::cout << "Failed.." << std::endl;
722 return false;
723}
724
725int CEditHandler::GetSize(const std::wstring &p1)
726{
727 std::ifstream in_file(p1.data(), std::ios::binary);
728 if(!in_file) return -1;
729
730 in_file.seekg(0, std::ios::end);
731 return in_file.tellg();
732}
733
734bool CEditHandler::SameFiles(const std::wstring &p1, const std::wstring &p2)
735{
736 std::ifstream f1(p1.data(), std::ifstream::binary | std::ifstream::ate);
737 std::ifstream f2(p2.data(), std::ifstream::binary | std::ifstream::ate);
738
739 // In case files cannot be opened
740 if (f1.fail() || f2.fail())
741 return false;
742
743 f1.ignore(std::numeric_limits<std::streamsize>::max());
744 f2.ignore(std::numeric_limits<std::streamsize>::max());
745
746 // Check if the file has the same size..
747 if(f1.gcount() != f2.gcount())
748 return false;
749
750 f1.clear(); // Since ignore will have set eof.
751 f1.seekg(0, std::ifstream::beg);
752
753 f2.clear(); // Since ignore will have set eof.
754 f2.seekg(0, std::ifstream::beg);
755
756 // Make a strict comparison of file buffers
757 return std::equal(std::istreambuf_iterator<char>(f1.rdbuf()),
758 std::istreambuf_iterator<char>(),
759 std::istreambuf_iterator<char>(f2.rdbuf()));
760}
761
762void CEditHandler::CheckForModifications(bool emitEvent)
763{
764 static bool insideCheckForModifications = false;
765 if (insideCheckForModifications) {
766 return;
767 }
768
769 if (emitEvent) {
770 QueueEvent(new wxCommandEvent(fzEDIT_CHANGEDFILE));
771 return;
772 }
773
774 insideCheckForModifications = true;
775
776 for (int i = 0; i < 2; ++i) {
777
778checkmodifications_loopbegin:
779 for (auto iter = m_fileDataList[i].begin(); iter != m_fileDataList[i].end(); ++iter) {
780 if (iter->state != edit) {
781 continue;
782 }
783
784 fz::datetime mtime;
785 bool is_link;
786 if (fz::local_filesys::get_file_info(fz::to_native(iter->localFile), is_link, 0, &mtime, 0) != fz::local_filesys::file) {
787 m_fileDataList[i].erase(iter);
788
789 // Evil goto. Imo the next C++ standard needs a comefrom keyword.
790 goto checkmodifications_loopbegin;
791 }
792
793 if (mtime.empty()) {
794 continue;
795 }
796
797 if (!iter->modificationTime.empty() && !iter->modificationTime.compare(mtime)) {
798 continue;
799 }
800
801 // File has changed, ask user what to do
802 m_busyTimer.Stop();
803 if (!wxDialogEx::CanShowPopupDialog()) {
804 m_busyTimer.Start(1000, true);
805 insideCheckForModifications = false;
806 return;
807 }
808 wxTopLevelWindow* pTopWindow = (wxTopLevelWindow*)wxTheApp->GetTopWindow();
809 if (pTopWindow && pTopWindow->IsIconized()) {
810 pTopWindow->RequestUserAttention(wxUSER_ATTENTION_INFO);
811 insideCheckForModifications = false;
812 return;
813 }
814
815 // Auto-upload: only considering files smaller than "maxsize" (usually text for quick edit of files..)
816 int size = GetSize(iter->localFile);
817 int maxsize = 10 * 1024*1024; // 10 MB
818 if(size < maxsize) {
819
820 // Change the local file variables
821 std::wstring localFileBak = std::wstring(iter->localFile);
822 std::wstring localFileCopy = GetTemporaryFile(std::wstring(iter->localFile + ".bak"));
823 if (! localFileCopy.empty()) {
824
825 localFileBak = iter->localFile;
826 iter->localFile = localFileCopy;
827 }
828
829 // Make a safe copy of the
830 bool autoupload = SafeCopy(localFileBak, localFileCopy);
831 if(autoupload) {
832
833 UploadFile(CEditHandler::fileType(i), iter, false);
834 std::remove((const char*) localFileCopy.c_str()); // Erase the backup file
835
836 // Put back local file variables
837 iter->localFile = localFileBak;
838 iter->modificationTime = mtime;
839 goto checkmodifications_loopbegin;
840 }
841 }
842
843 // Standard procedure with display notification
844 bool remove = false;
845 int res = DisplayChangeNotification(CEditHandler::fileType(i), *iter, remove);
846 if (res == -1)
847 continue;
848
849 if (res == wxID_YES) {
850 UploadFile(CEditHandler::fileType(i), iter, remove);
851 goto checkmodifications_loopbegin;
852
853 }
854 else if (remove && res != wxID_CANCEL) {
855 if (i == static_cast<int>(remote)) {
856 if (fz::local_filesys::get_file_info(fz::to_native(iter->localFile), is_link, 0, &mtime, 0) != fz::local_filesys::file || wxRemoveFile(iter->localFile)) {
857 m_fileDataList[i].erase(iter);
858 goto checkmodifications_loopbegin;
859 }
860 iter->state = removing;
861 }
862 else {
863 m_fileDataList[i].erase(iter);
864 goto checkmodifications_loopbegin;
865 }
866 }
867 else if (fz::local_filesys::get_file_info(fz::to_native(iter->localFile), is_link, 0, &mtime, 0) != fz::local_filesys::file) {
868 m_fileDataList[i].erase(iter);
869 goto checkmodifications_loopbegin;
870 }
871 else {
872 iter->modificationTime = mtime;
873 }
874 }
875 }
876
877 SetTimerState(100);
878 insideCheckForModifications = false;
879}
880
881int CEditHandler::DisplayChangeNotification(CEditHandler::fileType type, CEditHandler::t_fileData const& data, bool& remove)
882{
883 wxDialogEx dlg;
884
885 if (!dlg.Create(wxTheApp->GetTopWindow(), -1, _("File has changed"))) {
886 return -1;
887 }
888
889 auto& lay = dlg.layout();
890
891 auto main = lay.createMain(&dlg, 1);
892 main->AddGrowableCol(0);
893
894 main->Add(new wxStaticText(&dlg, -1, _("A file previously opened has been changed.")));
895
896 auto inner = lay.createFlex(2);
897 main->Add(inner);
898
899 inner->Add(new wxStaticText(&dlg, -1, _("Filename:")));
900 inner->Add(new wxStaticText(&dlg, -1, LabelEscape((type == local) ? data.localFile : data.remoteFile)));
901
902 if (type == remote) {
903 std::wstring file = data.localFile;
904 size_t pos = file.rfind(wxFileName::GetPathSeparator());
905 if (pos != std::wstring::npos) {
906 file = file.substr(pos + 1);
907 }
908 if (file != data.remoteFile) {
909 inner->Add(new wxStaticText(&dlg, -1, _("Opened as:")));
910 inner->Add(new wxStaticText(&dlg, -1, LabelEscape(file)));
911 }
912 }
913
914 inner->Add(new wxStaticText(&dlg, -1, _("Server:")));
915 inner->Add(new wxStaticText(&dlg, -1, LabelEscape(data.site.Format(ServerFormat::with_user_and_optional_port))));
916
917 inner->Add(new wxStaticText(&dlg, -1, _("Remote path:")));
918 inner->Add(new wxStaticText(&dlg, -1, LabelEscape(data.remotePath.GetPath())));
919
920 main->Add(new wxStaticLine(&dlg), lay.grow);
921
922 wxCheckBox* cb{};
923 if (type == local) {
924 main->Add(new wxStaticText(&dlg, -1, _("Upload this file to the server?")));
925 cb = new wxCheckBox(&dlg, -1, _("&Finish editing"));
926 }
927 else {
928 main->Add(new wxStaticText(&dlg, -1, _("Upload this file back to the server?")));
929 cb = new wxCheckBox(&dlg, -1, _("&Finish editing and delete local file"));
930
931 }
932 main->Add(cb);
933
934 auto buttons = lay.createButtonSizer(&dlg, main, false);
935 auto yes = new wxButton(&dlg, wxID_YES, _("&Yes"));
936 yes->SetDefault();
937 buttons->AddButton(yes);
938 auto no = new wxButton(&dlg, wxID_NO, _("&No"));
939 buttons->AddButton(no);
940 buttons->Realize();
941
942 yes->Bind(wxEVT_BUTTON, [&dlg](wxEvent const&) { dlg.EndDialog(wxID_YES); });
943 no->Bind(wxEVT_BUTTON, [&dlg](wxEvent const&) { dlg.EndDialog(wxID_NO); });
944
945 dlg.Layout();
946 dlg.GetSizer()->Fit(&dlg);
947
948 int res = dlg.ShowModal();
949
950 remove = cb->IsChecked();
951
952 return res;
953}
954
955bool CEditHandler::UploadFile(std::wstring const& file, CServerPath const& remotePath, Site const& site, bool unedit)
956{
957 std::list<t_fileData>::iterator iter = GetFile(file, remotePath, site);
958 return UploadFile(remote, iter, unedit);
959}
960
961bool CEditHandler::UploadFile(std::wstring const& file, bool unedit)
962{
963 std::list<t_fileData>::iterator iter = GetFile(file);
964 return UploadFile(local, iter, unedit);
965}
966
967bool CEditHandler::UploadFile(fileType type, std::list<t_fileData>::iterator iter, bool unedit)
968{
969 wxCHECK(type != none, false);
970
971 if (iter == m_fileDataList[type].end()) {
972 return false;
973 }
974
975 wxASSERT(iter->state == edit || iter->state == upload_and_remove_failed);
976 if (iter->state != edit && iter->state != upload_and_remove_failed) {
977 return false;
978 }
979
980 iter->state = unedit ? upload_and_remove : upload;
981
982 int64_t size;
983 fz::datetime mtime;
984
985 bool is_link;
986 if (fz::local_filesys::get_file_info(fz::to_native(iter->localFile), is_link, &size, &mtime, 0) != fz::local_filesys::file) {
987 m_fileDataList[type].erase(iter);
988 return false;
989 }
990
991 if (mtime.empty()) {
992 mtime = fz::datetime::now();
993 }
994
995 iter->modificationTime = mtime;
996
997 wxASSERT(m_pQueue);
998
999 std::wstring file;
1000 CLocalPath localPath(iter->localFile, &file);
1001 if (file.empty()) {
1002 m_fileDataList[type].erase(iter);
1003 return false;
1004 }
1005
1006 m_pQueue->QueueFile(false, false, file, (file == iter->remoteFile) ? std::wstring() : iter->remoteFile, localPath, iter->remotePath, iter->site, size, type, QueuePriority::high);
1007 m_pQueue->QueueFile_Finish(true);
1008
1009 return true;
1010}
1011
1012void CEditHandler::OnTimerEvent(wxTimerEvent&)
1013{
1014#ifdef __WXMSW__
1015 // Don't check for changes if mouse is captured,
1016 // e.g. if user is dragging a file
1017 if (GetCapture()) {
1018 return;
1019 }
1020#endif
1021
1022 CheckForModifications(true);
1023}
1024
1025void CEditHandler::SetTimerState(int t)
1026{
1027 bool editing = GetFileCount(none, edit) != 0;
1028
1029 if (m_timer.IsRunning()) {
1030 if (!editing) {
1031 m_timer.Stop();
1032 }
1033 }
1034 else if (editing) {
1035 m_timer.Start(t);
1036 }
1037}
1038
1039std::vector<std::wstring> CEditHandler::CanOpen(std::wstring const& fileName, bool &program_exists)
1040{
1041 auto cmd_with_args = GetAssociation(fileName);
1042 if (cmd_with_args.empty()) {
1043 return cmd_with_args;
1044 }
1045
1046 program_exists = ProgramExists(cmd_with_args.front());
1047
1048 return cmd_with_args;
1049}
1050
1051std::vector<std::wstring> CEditHandler::GetAssociation(std::wstring const& file)
1052{
1053 std::vector<std::wstring> ret;
1054
1055 if (!COptions::Get()->get_int(OPTION_EDIT_ALWAYSDEFAULT)) {
1056 ret = GetCustomAssociation(file);
1057 }
1058
1059 if (ret.empty()) {
1060 std::wstring command = COptions::Get()->get_string(OPTION_EDIT_DEFAULTEDITOR);
1061 if (!command.empty()) {
1062 if (command[0] == '1') {
1063 // Text editor
1064 ret = GetSystemAssociation(L"foo.txt");
1065 }
1066 else if (command[0] == '2') {
1067 ret = UnquoteCommand(std::wstring_view(command).substr(1));
1068 }
1069 }
1070 }
1071
1072 return ret;
1073}
1074
1075std::vector<std::wstring> CEditHandler::GetCustomAssociation(std::wstring_view const& file)
1076{
1077 std::vector<std::wstring> ret;
1078
1079 std::wstring ext = GetExtension(file);
1080 if (ext.empty()) {
1081 ext = L"/";
1082 }
1083
1084 auto assocs = LoadAssociations();
1085 auto it = assocs.find(ext);
1086 if (it != assocs.end()) {
1087 ret = it->second;
1088 }
1089 return ret;
1090}
1091
1092void CEditHandler::OnChangedFileEvent(wxCommandEvent&)
1093{
1094 CheckForModifications();
1095}
1096
1097std::wstring CEditHandler::GetTemporaryFile(std::wstring name)
1098{
1099 name = CQueueView::ReplaceInvalidCharacters(name, true);
1100#ifdef __WXMSW__
1101 // MAX_PATH - 1 is theoretical limit, we subtract another 4 to allow
1102 // editors which create temporary files
1103 size_t max = MAX_PATH - 5;
1104#else
1105 size_t max = std::wstring::npos;
1106#endif
1107 if (max != std::wstring::npos) {
1108 name = TruncateFilename(m_localDir, name, max);
1109 if (name.empty()) {
1110 return std::wstring();
1111 }
1112 }
1113
1114 std::wstring file = m_localDir + name;
1115 if (!FilenameExists(file)) {
1116 return file;
1117 }
1118
1119 if (max != std::wstring::npos) {
1120 --max;
1121 }
1122 int cutoff = 1;
1123 int n = 1;
1124 while (++n < 10000) { // Just to give up eventually
1125 // Further reduce length if needed
1126 if (max != std::wstring::npos && n >= cutoff) {
1127 cutoff *= 10;
1128 --max;
1129 name = TruncateFilename(m_localDir, name, max);
1130 if (name.empty()) {
1131 return std::wstring();
1132 }
1133 }
1134
1135 size_t pos = name.rfind('.');
1136 if (pos == std::wstring::npos || !pos) {
1137 file = m_localDir + name + fz::sprintf(L" %d", n);
1138 }
1139 else {
1140 file = m_localDir + name.substr(0, pos) + fz::sprintf(L" %d", n) + name.substr(pos);
1141 }
1142
1143 if (!FilenameExists(file)) {
1144 return file;
1145 }
1146 }
1147
1148 return std::wstring();
1149}
1150
1151std::wstring CEditHandler::TruncateFilename(std::wstring const& path, std::wstring const& name, size_t max)
1152{
1153 size_t const pathlen = path.size();
1154 size_t const namelen = name.size();
1155
1156 if (namelen + pathlen > max) {
1157 size_t pos = name.rfind('.');
1158 if (pos != std::wstring::npos) {
1159 size_t extlen = namelen - pos;
1160 if (pathlen + extlen >= max)
1161 {
1162 // Cannot truncate extension
1163 return std::wstring();
1164 }
1165
1166 return name.substr(0, max - pathlen - extlen) + name.substr(pos);
1167 }
1168 }
1169
1170 return name;
1171}
1172
1173bool CEditHandler::FilenameExists(std::wstring const& file)
1174{
1175 for (auto const& fileData : m_fileDataList[remote]) {
1176 // Always ignore case, we don't know which type of filesystem the user profile
1177 // is installed upon.
1178 if (!wxString(fileData.localFile).CmpNoCase(file)) {
1179 return true;
1180 }
1181 }
1182
1183 if (wxFileName::FileExists(file)) {
1184 // Save to remove, it's not marked as edited anymore.
1185 {
1186 wxLogNull log;
1187 wxRemoveFile(file);
1188 }
1189
1190 if (wxFileName::FileExists(file)) {
1191 return true;
1192 }
1193 }
1194
1195 return false;
1196}
1197
1198
1199
1200bool CEditHandler::Edit(CEditHandler::fileType type, std::wstring const& fileName, CServerPath const& path, Site const& site, int64_t size, wxWindow* parent)
1201{
1202 std::vector<FileData> data;
1203 FileData d{fileName, size};
1204 data.push_back(d);
1205
1206 return Edit(type, data, path, site, parent);
1207}
1208
1209bool CEditHandler::Edit(CEditHandler::fileType type, std::vector<FileData> const& data, CServerPath const& path, Site const& site, wxWindow* parent)
1210{
1211 if (type == CEditHandler::remote) {
1212 std::wstring const& localDir = GetLocalDirectory();
1213 if (localDir.empty()) {
1214 wxMessageBoxEx(_("Could not get temporary directory to download file into."), _("Cannot edit file"), wxICON_STOP);
1215 return false;
1216 }
1217 }
1218
1219 if (data.empty()) {
1220 wxBell();
1221 return false;
1222 }
1223
1224 if (data.size() > 10) {
1225 CConditionalDialog dlg(parent, CConditionalDialog::many_selected_for_edit, CConditionalDialog::yesno);
1226 dlg.SetTitle(_("Confirmation needed"));
1227 dlg.AddText(_("You have selected more than 10 files for editing, do you really want to continue?"));
1228
1229 if (!dlg.Run()) {
1230 return false;
1231 }
1232 }
1233
1234 bool success = true;
1235 int already_editing_action{};
1236 for (auto const& file : data) {
1237 if (!DoEdit(type, file, path, site, parent, data.size(), already_editing_action)) {
1238 success = false;
1239 }
1240 }
1241
1242 return success;
1243}
1244
1245bool CEditHandler::DoEdit(CEditHandler::fileType type, FileData const& file, CServerPath const& path, Site const& site, wxWindow* parent, size_t fileCount, int& already_editing_action)
1246{
1247 for (auto const& c : GetExtension(file.name)) {
1248 if (c < 32 && c != '\t') {
1249 wxMessageBoxEx(_("Forbidden character in file extension."), _("Cannot view/edit selected file"), wxICON_EXCLAMATION);
1250 return false;
1251 }
1252 }
1253
1254 // First check whether this file is already being edited
1255 fileState state;
1256 if (type == local) {
1257 state = GetFileState(file.name);
1258 }
1259 else {
1260 state = GetFileState(file.name, path, site);
1261 }
1262 switch (state)
1263 {
1264 case CEditHandler::download:
1265 case CEditHandler::upload:
1266 case CEditHandler::upload_and_remove:
1267 case CEditHandler::upload_and_remove_failed:
1268 wxMessageBoxEx(_("A file with that name is already being transferred."), _("Cannot view/edit selected file"), wxICON_EXCLAMATION);
1269 return false;
1270 case CEditHandler::removing:
1271 if (!Remove(file.name, path, site)) {
1272 wxMessageBoxEx(_("A file with that name is still being edited. Please close it and try again."), _("Selected file is already opened"), wxICON_EXCLAMATION);
1273 return false;
1274 }
1275 break;
1276 case CEditHandler::edit:
1277 {
1278 int action = already_editing_action;
1279 if (!action) {
1280 wxDialogEx dlg;
1281 if (!dlg.Create(parent, -1, _("Selected file already being edited"))) {
1282 wxBell();
1283 return false;
1284 }
1285
1286 auto& lay = dlg.layout();
1287 auto main = lay.createMain(&dlg, 1);
1288 main->AddGrowableCol(0);
1289
1290 main->Add(new wxStaticText(&dlg, -1, _("The selected file is already being edited:")));
1291 main->Add(new wxStaticText(&dlg, -1, LabelEscape(file.name)));
1292
1293 main->AddSpacer(0);
1294
1295 int choices = COptions::Get()->get_int(OPTION_PERSISTENT_CHOICES);
1296
1297 wxRadioButton* reopen{};
1298 if (type == local) {
1299 main->Add(new wxStaticText(&dlg, -1, _("Do you want to reopen this file?")));
1300 }
1301 else {
1302 main->Add(new wxStaticText(&dlg, -1, _("Action to perform:")));
1303
1304 reopen = new wxRadioButton(&dlg, -1, _("&Reopen local file"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP);
1305 main->Add(reopen);
1306 wxRadioButton* retransfer = new wxRadioButton(&dlg, -1, _("&Discard local file then download and edit file anew"));
1307 main->Add(retransfer);
1308
1309 if (choices & edit_choices::edit_existing_action) {
1310 retransfer->SetValue(true);
1311 }
1312 else {
1313 reopen->SetValue(true);
1314 }
1315 }
1316
1317 wxCheckBox* always{};
1318 if (fileCount > 1) {
1319 always = new wxCheckBox(&dlg, -1, _("Do the same with &all selected files already being edited"));
1320 main->Add(always);
1321 if (choices & edit_choices::edit_existing_always) {
1322 always->SetValue(true);
1323 }
1324 }
1325
1326 auto buttons = lay.createButtonSizer(&dlg, main, true);
1327
1328 if (type == remote) {
1329 auto ok = new wxButton(&dlg, wxID_OK, _("OK"));
1330 ok->SetDefault();
1331 buttons->AddButton(ok);
1332 auto cancel = new wxButton(&dlg, wxID_CANCEL, _("Cancel"));
1333 buttons->AddButton(cancel);
1334 }
1335 else {
1336 auto yes = new wxButton(&dlg, wxID_YES, _("&Yes"));
1337 yes->SetDefault();
1338 buttons->AddButton(yes);
1339 auto no = new wxButton(&dlg, wxID_NO, _("&No"));
1340 buttons->AddButton(no);
1341 yes->Bind(wxEVT_BUTTON, [&dlg](wxEvent const&) { dlg.EndDialog(wxID_YES); });
1342 no->Bind(wxEVT_BUTTON, [&dlg](wxEvent const&) { dlg.EndDialog(wxID_NO); });
1343 }
1344 buttons->Realize();
1345
1346 dlg.GetSizer()->Fit(&dlg);
1347 int res = dlg.ShowModal();
1348 if (res != wxID_OK && res != wxID_YES) {
1349 wxBell();
1350 action = -1;
1351 }
1352 else if (type == CEditHandler::local || (reopen && reopen->GetValue())) {
1353 action = 1;
1354 if (type == CEditHandler::remote) {
1355 choices &= ~edit_choices::edit_existing_action;
1356 }
1357 }
1358 else {
1359 action = 2;
1360 choices |= edit_choices::edit_existing_action;
1361 }
1362
1363 if (fileCount > 1) {
1364 if (always && always->GetValue()) {
1365 already_editing_action = action;
1366 choices |= edit_choices::edit_existing_always;
1367 }
1368 else {
1369 choices &= ~edit_choices::edit_existing_always;
1370 }
1371 }
1372 COptions::Get()->set(OPTION_PERSISTENT_CHOICES, choices);
1373 }
1374
1375 if (action == -1) {
1376 return false;
1377 }
1378 else if (action == 1) {
1379 if (type == CEditHandler::local) {
1380 LaunchEditor(file.name);
1381 }
1382 else {
1383 LaunchEditor(file.name, path, site);
1384 }
1385 return true;
1386 }
1387 else {
1388 if (!Remove(file.name, path, site)) {
1389 wxMessageBoxEx(_("The selected file is still opened in some other program, please close it."), _("Selected file is still being edited"), wxICON_EXCLAMATION);
1390 return false;
1391 }
1392 }
1393 }
1394 break;
1395 default:
1396 break;
1397 }
1398
1399 // Create local filename if needed
1400 std::wstring localFile;
1401 std::wstring remoteFile;
1402 if (type == fileType::local) {
1403 localFile = file.name;
1404
1405 CLocalPath localPath(localFile, &remoteFile);
1406 if (localPath.empty()) {
1407 wxBell();
1408 return false;
1409 }
1410 }
1411 else {
1412 localFile = GetTemporaryFile(file.name);
1413 if (localFile.empty()) {
1414 wxMessageBoxEx(_("Could not create temporary file name."), _("Cannot view/edit selected file"), wxICON_EXCLAMATION);
1415 return false;
1416 }
1417 remoteFile = file.name;
1418 }
1419
1420
1421 // Find associated program
1422 bool program_exists = false;
1423 std::vector<std::wstring> cmd_with_args;
1424 if (!wxGetKeyState(WXK_SHIFT) || COptions::Get()->get_int(OPTION_EDIT_ALWAYSDEFAULT)) {
1425 cmd_with_args = CanOpen(file.name, program_exists);
1426 }
1427 if (cmd_with_args.empty()) {
1428 CNewAssociationDialog dlg(parent);
1429 if (!dlg.Run(file.name)) {
1430 return false;
1431 }
1432 cmd_with_args = CanOpen(file.name, program_exists);
1433 if (cmd_with_args.empty()) {
1434 wxMessageBoxEx(wxString::Format(_("The file '%s' could not be opened:\nNo program has been associated on your system with this file type."), file.name), _("Opening failed"), wxICON_EXCLAMATION);
1435 return false;
1436 }
1437 }
1438 if (!program_exists) {
1439 wxString msg = wxString::Format(_("The file '%s' cannot be opened:\nThe associated program (%s) could not be found.\nPlease check your filetype associations."), file.name, QuoteCommand(cmd_with_args));
1440 wxMessageBoxEx(msg, _("Cannot edit file"), wxICON_EXCLAMATION);
1441 return false;
1442 }
1443
1444 // We can proceed with adding the item and either open it or transfer it.
1445 return AddFile(type, localFile, remoteFile, path, site, file.size);
1446}
1447
1448
1449#define COLUMN_NAME 0
1450#define COLUMN_TYPE 1
1451#define COLUMN_REMOTEPATH 2
1452#define COLUMN_STATUS 3
1453
1454struct CEditHandlerStatusDialog::impl final
1455{
1456 wxWindow* parent_{};
1457
1458 wxListCtrlEx* listCtrl_{};
1459
1460 wxButton* unedit_{};
1461 wxButton* upload_{};
1462 wxButton* upload_and_unedit_{};
1463 wxButton* open_{};
1464 CEditHandler* editHandler_{};
1465
1466 std::unique_ptr<CWindowStateManager> windowStateManager_;
1467};
1468
1469CEditHandlerStatusDialog::CEditHandlerStatusDialog(wxWindow* parent)
1470 : impl_(std::make_unique<impl>())
1471{
1472 impl_->parent_ = parent;
1473}
1474
1475CEditHandlerStatusDialog::~CEditHandlerStatusDialog()
1476{
1477 if (impl_ && impl_->windowStateManager_) {
1478 impl_->windowStateManager_->Remember(OPTION_EDITSTATUSDIALOG_SIZE);
1479 }
1480}
1481
1482int CEditHandlerStatusDialog::ShowModal()
1483{
1484 impl_->editHandler_ = CEditHandler::Get();
1485 if (!impl_->editHandler_) {
1486 return wxID_CANCEL;
1487 }
1488
1489 if (!impl_->editHandler_->GetFileCount(CEditHandler::none, CEditHandler::unknown)) {
1490 wxMessageBoxEx(_("No files are currently being edited."), _("Cannot show dialog"), wxICON_INFORMATION, impl_->parent_);
1491 return wxID_CANCEL;
1492 }
1493
1494 if (!Create(impl_->parent_, -1, _("Files currently being edited"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)) {
1495 return wxID_CANCEL;
1496 }
1497
1498 auto & lay = layout();
1499 auto main = lay.createMain(this, 1);
1500
1501 main->Add(new wxStaticText(this, -1, _("The &following files are currently being edited:")));
1502
1503 impl_->listCtrl_ = new wxListCtrlEx(this, -1, wxDefaultPosition, wxDefaultSize, wxLC_REPORT);
1504 impl_->listCtrl_->SetFocus();
1505 main->Add(impl_->listCtrl_, lay.grow);
1506 main->AddGrowableCol(0);
1507 main->AddGrowableRow(1);
1508
1509 impl_->listCtrl_->InsertColumn(0, _("Filename"));
1510 impl_->listCtrl_->InsertColumn(1, _("Type"));
1511 impl_->listCtrl_->InsertColumn(2, _("Remote path"));
1512 impl_->listCtrl_->InsertColumn(3, _("Status"));
1513
1514 {
1515 const std::list<CEditHandler::t_fileData>& files = impl_->editHandler_->GetFiles(CEditHandler::remote);
1516 unsigned int i = 0;
1517 for (std::list<CEditHandler::t_fileData>::const_iterator iter = files.begin(); iter != files.end(); ++iter, ++i) {
1518 impl_->listCtrl_->InsertItem(i, iter->remoteFile);
1519 impl_->listCtrl_->SetItem(i, COLUMN_TYPE, _("Remote"));
1520 switch (iter->state)
1521 {
1522 case CEditHandler::download:
1523 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Downloading"));
1524 break;
1525 case CEditHandler::upload:
1526 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Uploading"));
1527 break;
1528 case CEditHandler::upload_and_remove:
1529 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Uploading and pending removal"));
1530 break;
1531 case CEditHandler::upload_and_remove_failed:
1532 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Upload failed"));
1533 break;
1534 case CEditHandler::removing:
1535 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Pending removal"));
1536 break;
1537 case CEditHandler::edit:
1538 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Being edited"));
1539 break;
1540 default:
1541 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Unknown"));
1542 break;
1543 }
1544 impl_->listCtrl_->SetItem(i, COLUMN_REMOTEPATH, iter->site.Format(ServerFormat::with_user_and_optional_port) + iter->remotePath.GetPath());
1545 CEditHandler::t_fileData* pData = new CEditHandler::t_fileData(*iter);
1546 impl_->listCtrl_->SetItemPtrData(i, (wxUIntPtr)pData);
1547 }
1548 }
1549
1550 {
1551 const std::list<CEditHandler::t_fileData>& files = impl_->editHandler_->GetFiles(CEditHandler::local);
1552 unsigned int i = 0;
1553 for (std::list<CEditHandler::t_fileData>::const_iterator iter = files.begin(); iter != files.end(); ++iter, ++i) {
1554 impl_->listCtrl_->InsertItem(i, iter->localFile);
1555 impl_->listCtrl_->SetItem(i, COLUMN_TYPE, _("Local"));
1556 switch (iter->state)
1557 {
1558 case CEditHandler::upload:
1559 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Uploading"));
1560 break;
1561 case CEditHandler::upload_and_remove:
1562 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Uploading and unediting"));
1563 break;
1564 case CEditHandler::edit:
1565 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Being edited"));
1566 break;
1567 default:
1568 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Unknown"));
1569 break;
1570 }
1571 impl_->listCtrl_->SetItem(i, COLUMN_REMOTEPATH, iter->site.Format(ServerFormat::with_user_and_optional_port) + iter->remotePath.GetPath());
1572 CEditHandler::t_fileData* pData = new CEditHandler::t_fileData(*iter);
1573 impl_->listCtrl_->SetItemPtrData(i, (wxUIntPtr)pData);
1574 }
1575 }
1576
1577 for (int i = 0; i < 4; ++i) {
1578 impl_->listCtrl_->SetColumnWidth(i, wxLIST_AUTOSIZE);
1579 }
1580 impl_->listCtrl_->SetMinSize(wxSize(impl_->listCtrl_->GetColumnWidth(0) + impl_->listCtrl_->GetColumnWidth(1) + impl_->listCtrl_->GetColumnWidth(2) + impl_->listCtrl_->GetColumnWidth(3) + lay.dlgUnits(10), impl_->listCtrl_->GetMinSize().GetHeight()));
1581
1582 auto onsel = [this](wxListEvent const&) { SetCtrlState(); };
1583 impl_->listCtrl_->Bind(wxEVT_LIST_ITEM_SELECTED, onsel);
1584 impl_->listCtrl_->Bind(wxEVT_LIST_ITEM_DESELECTED, onsel);
1585
1586 main->Add(new wxStaticText(this, -1, _("Action on selected file:")));
1587
1588 auto inner = lay.createGrid(2, 2);
1589 main->Add(inner, lay.halign);
1590
1591 impl_->unedit_ = new wxButton(this, -1, _("&Unedit"));
1592 impl_->unedit_->Bind(wxEVT_BUTTON, [this](wxCommandEvent const&) { OnUnedit(); });
1593 inner->Add(impl_->unedit_, lay.valigng);
1594 impl_->upload_ = new wxButton(this, -1, _("U&pload"));
1595 impl_->upload_->Bind(wxEVT_BUTTON, [this](wxCommandEvent const&) { OnUpload(false); });
1596 inner->Add(impl_->upload_, lay.valigng);
1597 impl_->upload_and_unedit_ = new wxButton(this, -1, _("Up&load and unedit"));
1598 impl_->upload_and_unedit_->Bind(wxEVT_BUTTON, [this](wxCommandEvent const&) { OnUpload(true); });
1599 inner->Add(impl_->upload_and_unedit_, lay.valigng);
1600 impl_->open_ = new wxButton(this, -1, _("Op&en file"));
1601 impl_->open_->Bind(wxEVT_BUTTON, [this](wxCommandEvent const&) { OnEdit(); });
1602 inner->Add(impl_->open_, lay.valigng);
1603
1604
1605 auto buttons = lay.createButtonSizer(this, main, true);
1606 auto ok = new wxButton(this, wxID_OK, _("OK"));
1607 ok->SetDefault();
1608 buttons->AddButton(ok);
1609 buttons->Realize();
1610
1611 GetSizer()->Fit(this);
1612 SetMinClientSize(GetSizer()->GetMinSize());
1613
1614 impl_->windowStateManager_ = std::make_unique<CWindowStateManager>(static_cast<wxTopLevelWindow*>(this));
1615 impl_->windowStateManager_->Restore(OPTION_EDITSTATUSDIALOG_SIZE, GetSize());
1616
1617 SetCtrlState();
1618
1619 int res = wxDialogEx::ShowModal();
1620
1621 for (int i = 0; i < impl_->listCtrl_->GetItemCount(); ++i) {
1622 delete (CEditHandler::t_fileData*)impl_->listCtrl_->GetItemData(i);
1623 }
1624
1625 return res;
1626}
1627
1628void CEditHandlerStatusDialog::SetCtrlState()
1629{
1630 bool selectedEdited = false;
1631 bool selectedOther = false;
1632 bool selectedUploadRemoveFailed = false;
1633
1634 int item = -1;
1635 while ((item = impl_->listCtrl_->GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)) != -1) {
1636 CEditHandler::fileType type;
1637 CEditHandler::t_fileData* pData = GetDataFromItem(item, type);
1638 if (pData->state == CEditHandler::edit) {
1639 selectedEdited = true;
1640 }
1641 else if (pData->state == CEditHandler::upload_and_remove_failed) {
1642 selectedUploadRemoveFailed = true;
1643 }
1644 else {
1645 selectedOther = true;
1646 }
1647 }
1648
1649 bool const select = selectedEdited && !selectedOther && !selectedUploadRemoveFailed;
1650 impl_->unedit_->Enable(select || (!selectedOther && selectedUploadRemoveFailed));
1651 impl_->upload_->Enable(select || (!selectedEdited && !selectedOther && selectedUploadRemoveFailed));
1652 impl_->upload_and_unedit_->Enable(select || (!selectedEdited && !selectedOther && selectedUploadRemoveFailed));
1653 impl_->open_->Enable(select);
1654}
1655
1656void CEditHandlerStatusDialog::OnUnedit()
1657{
1658 std::list<int> files;
1659 int item = -1;
1660 while ((item = impl_->listCtrl_->GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)) != -1) {
1661 impl_->listCtrl_->SetItemState(item, 0, wxLIST_STATE_SELECTED);
1662 CEditHandler::fileType type;
1663 CEditHandler::t_fileData* pData = GetDataFromItem(item, type);
1664 if (pData->state != CEditHandler::edit && pData->state != CEditHandler::upload_and_remove_failed) {
1665 wxBell();
1666 return;
1667 }
1668
1669 files.push_front(item);
1670 }
1671
1672 for (std::list<int>::const_iterator iter = files.begin(); iter != files.end(); ++iter) {
1673 const int i = *iter;
1674
1675 CEditHandler::fileType type;
1676 CEditHandler::t_fileData* pData = GetDataFromItem(i, type);
1677
1678 if (type == CEditHandler::local) {
1679 impl_->editHandler_->Remove(pData->localFile);
1680 delete pData;
1681 impl_->listCtrl_->DeleteItem(i);
1682 }
1683 else {
1684 if (impl_->editHandler_->Remove(pData->remoteFile, pData->remotePath, pData->site)) {
1685 delete pData;
1686 impl_->listCtrl_->DeleteItem(i);
1687 }
1688 else {
1689 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Pending removal"));
1690 }
1691 }
1692 }
1693
1694 SetCtrlState();
1695}
1696
1697void CEditHandlerStatusDialog::OnUpload(bool unedit_after)
1698{
1699 std::list<int> files;
1700 int item = -1;
1701 while ((item = impl_->listCtrl_->GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)) != -1) {
1702 impl_->listCtrl_->SetItemState(item, 0, wxLIST_STATE_SELECTED);
1703
1704 CEditHandler::fileType type;
1705 CEditHandler::t_fileData* pData = GetDataFromItem(item, type);
1706
1707 if (pData->state != CEditHandler::edit && pData->state != CEditHandler::upload_and_remove_failed) {
1708 wxBell();
1709 return;
1710 }
1711 files.push_front(item);
1712 }
1713
1714 for (std::list<int>::const_iterator iter = files.begin(); iter != files.end(); ++iter) {
1715 const int i = *iter;
1716
1717 CEditHandler::fileType type;
1718 CEditHandler::t_fileData* pData = GetDataFromItem(i, type);
1719
1720 bool unedit = unedit_after || pData->state == CEditHandler::upload_and_remove_failed;
1721
1722 if (type == CEditHandler::local) {
1723 impl_->editHandler_->UploadFile(pData->localFile, unedit);
1724 }
1725 else {
1726 impl_->editHandler_->UploadFile(pData->remoteFile, pData->remotePath, pData->site, unedit);
1727 }
1728
1729 if (!unedit) {
1730 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Uploading"));
1731 }
1732 else if (type == CEditHandler::remote) {
1733 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Uploading and pending removal"));
1734 }
1735 else {
1736 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Uploading and unediting"));
1737 }
1738 }
1739
1740 SetCtrlState();
1741}
1742
1743void CEditHandlerStatusDialog::OnEdit()
1744{
1745 std::list<int> files;
1746 int item = -1;
1747 while ((item = impl_->listCtrl_->GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)) != -1) {
1748 impl_->listCtrl_->SetItemState(item, 0, wxLIST_STATE_SELECTED);
1749
1750 CEditHandler::fileType type;
1751 CEditHandler::t_fileData* pData = GetDataFromItem(item, type);
1752
1753 if (pData->state != CEditHandler::edit) {
1754 wxBell();
1755 return;
1756 }
1757 files.push_front(item);
1758 }
1759
1760 for (std::list<int>::const_iterator iter = files.begin(); iter != files.end(); ++iter) {
1761 const int i = *iter;
1762
1763 CEditHandler::fileType type;
1764 CEditHandler::t_fileData* pData = GetDataFromItem(i, type);
1765
1766 if (type == CEditHandler::local) {
1767 if (!impl_->editHandler_->LaunchEditor(pData->localFile)) {
1768 if (impl_->editHandler_->Remove(pData->localFile)) {
1769 delete pData;
1770 impl_->listCtrl_->DeleteItem(i);
1771 }
1772 else {
1773 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Pending removal"));
1774 }
1775 }
1776 }
1777 else {
1778 if (!impl_->editHandler_->LaunchEditor(pData->remoteFile, pData->remotePath, pData->site)) {
1779 if (impl_->editHandler_->Remove(pData->remoteFile, pData->remotePath, pData->site)) {
1780 delete pData;
1781 impl_->listCtrl_->DeleteItem(i);
1782 }
1783 else {
1784 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Pending removal"));
1785 }
1786 }
1787 }
1788 }
1789
1790 SetCtrlState();
1791}
1792
1793CEditHandler::t_fileData* CEditHandlerStatusDialog::GetDataFromItem(int item, CEditHandler::fileType &type)
1794{
1795 CEditHandler::t_fileData* pData = (CEditHandler::t_fileData*)impl_->listCtrl_->GetItemData(item);
1796 wxASSERT(pData);
1797
1798 wxListItem info;
1799 info.SetMask(wxLIST_MASK_TEXT);
1800 info.SetId(item);
1801 info.SetColumn(1);
1802 impl_->listCtrl_->GetItem(info);
1803 if (info.GetText() == _("Local")) {
1804 type = CEditHandler::local;
1805 }
1806 else {
1807 type = CEditHandler::remote;
1808 }
1809
1810 return pData;
1811}
1812
1813
1814
1815struct CNewAssociationDialog::impl
1816{
1817 wxRadioButton* rbSystem_{};
1818 wxRadioButton* rbDefault_{};
1819 wxRadioButton* rbCustom_{};
1820
1821 wxCheckBox* always_{};
1822
1823 wxTextCtrlEx* custom_{};
1824 wxButton* browse_{};
1825};
1826
1827CNewAssociationDialog::CNewAssociationDialog(wxWindow *parent)
1828 : parent_(parent)
1829{
1830}
1831
1832CNewAssociationDialog::~CNewAssociationDialog()
1833{
1834}
1835
1836void ShowQuotingRules(wxWindow* parent);
1837
1838bool CNewAssociationDialog::Run(std::wstring const& file)
1839{
1840 file_ = file;
1841
1842 ext_ = GetExtension(file);
1843
1844 impl_ = std::make_unique<impl>();
1845
1846 Create(parent_, -1, _("No program associated with filetype"));
1847
1848 auto & lay = layout();
1849
1850 auto * main = lay.createMain(this, 1);
1851
1852 if (ext_.empty()) {
1853 main->Add(new wxStaticText(this, -1, _("No program has been associated to edit extensionless files.")));
1854 }
1855 else if (ext_ == L".") {
1856 main->Add(new wxStaticText(this, -1, _("No program has been associated to edit dotfiles.")));
1857 }
1858 else {
1859 main->Add(new wxStaticText(this, -1, wxString::Format(_("No program has been associated to edit files with the extension '%s'."), LabelEscape(ext_))));
1860 }
1861
1862 main->Add(new wxStaticText(this, -1, _("Select how these files should be opened.")));
1863
1864 {
1865 auto const cmd_with_args = GetSystemAssociation(file);
1866 if (!cmd_with_args.empty()) {
1867 impl_->rbSystem_ = new wxRadioButton(this, -1, _("Use system association"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP);
1868 impl_->rbSystem_->Bind(wxEVT_RADIOBUTTON, [this](wxEvent const&) { SetCtrlState(); });
1869 impl_->rbSystem_->SetValue(true);
1870 main->Add(impl_->rbSystem_);
1871 main->Add(new wxStaticText(this, -1, _("The default editor for this file type is:") + L" " + LabelEscape(QuoteCommand(cmd_with_args))), 0, wxLEFT, lay.indent);
1872 }
1873 }
1874
1875 {
1876 auto const cmd_with_args = GetSystemAssociation(L"foo.txt");
1877 if (!cmd_with_args.empty()) {
1878 impl_->rbDefault_ = new wxRadioButton(this, -1, _("Use &default editor for text files"), wxDefaultPosition, wxDefaultSize, impl_->rbSystem_ ? 0 : wxRB_GROUP);
1879 impl_->rbDefault_->Bind(wxEVT_RADIOBUTTON, [this](wxEvent const&) { SetCtrlState(); });
1880 if (!impl_->rbSystem_) {
1881 impl_->rbDefault_->SetValue(true);
1882 }
1883 main->Add(impl_->rbDefault_);
1884 main->Add(new wxStaticText(this, -1, _("The default editor for text files is:") + " " + LabelEscape(QuoteCommand(cmd_with_args))), 0, wxLEFT, lay.indent);
1885 impl_->always_ = new wxCheckBox(this, -1, _("&Always use selection for all unassociated files"));
1886 main->Add(impl_->always_, 0, wxLEFT, lay.indent);
1887 }
1888 }
1889
1890 impl_->rbCustom_ = new wxRadioButton(this, -1, _("&Use custom program"), wxDefaultPosition, wxDefaultSize, (impl_->rbSystem_ || impl_->rbDefault_) ? 0 : wxRB_GROUP);
1891 impl_->rbCustom_->Bind(wxEVT_RADIOBUTTON, [this](wxEvent const&) { SetCtrlState(); });
1892 if (!impl_->rbSystem_ && !impl_->rbDefault_) {
1893 impl_->rbCustom_->SetValue(true);
1894 }
1895 main->Add(impl_->rbCustom_);
1896 auto row = lay.createFlex(2);
1897 row->AddGrowableCol(0);
1898 main->Add(row, 0, wxLEFT|wxGROW, lay.indent);
1899
1900 auto rules = new wxHyperlinkCtrl(this, -1, _("Quoting rules"), wxString());
1901 main->Add(rules, 0, wxLEFT, lay.indent);
1902 rules->Bind(wxEVT_HYPERLINK, [this](wxHyperlinkEvent const&) { ShowQuotingRules(this); });
1903
1904
1905 impl_->custom_ = new wxTextCtrlEx(this, -1, wxString());
1906 row->Add(impl_->custom_, lay.valigng);
1907 impl_->browse_ = new wxButton(this, -1, _("&Browse..."));
1908 impl_->browse_->Bind(wxEVT_BUTTON, [this](wxEvent const&) { OnBrowseEditor(); });
1909 row->Add(impl_->browse_, lay.valign);
1910
1911 auto buttons = lay.createButtonSizer(this, main, true);
1912 auto ok = new wxButton(this, wxID_OK, _("OK"));
1913 ok->SetDefault();
1914 buttons->AddButton(ok);
1915 auto cancel = new wxButton(this, wxID_CANCEL, _("Cancel"));
1916 buttons->AddButton(cancel);
1917 buttons->Realize();
1918
1919 ok->Bind(wxEVT_BUTTON, [this](wxEvent const&) { OnOK(); });
1920
1921 Layout();
1922 GetSizer()->Fit(this);
1923
1924 SetCtrlState();
1925
1926 return ShowModal() == wxID_OK;
1927}
1928
1929void CNewAssociationDialog::SetCtrlState()
1930{
1931 if (impl_->custom_) {
1932 impl_->custom_->Enable(impl_->rbCustom_ && impl_->rbCustom_->GetValue());
1933 }
1934 if (impl_->browse_) {
1935 impl_->browse_->Enable(impl_->rbCustom_ && impl_->rbCustom_->GetValue());
1936 }
1937 if (impl_->always_) {
1938 impl_->always_->Enable(impl_->rbDefault_ && impl_->rbDefault_->GetValue());
1939 }
1940}
1941
1942void CNewAssociationDialog::OnOK()
1943{
1944 const bool custom = impl_->rbCustom_ && impl_->rbCustom_->GetValue();
1945 const bool def = impl_->rbDefault_ && impl_->rbDefault_->GetValue();
1946 const bool always = impl_->always_ && impl_->always_->GetValue();
1947
1948 if (def && always) {
1949 COptions::Get()->set(OPTION_EDIT_DEFAULTEDITOR, _T("1"));
1950 EndModal(wxID_OK);
1951
1952 return;
1953 }
1954
1955 std::vector<std::wstring> cmd_with_args;
1956 if (custom) {
1957 std::wstring cmd = impl_->custom_->GetValue().ToStdWstring();
1958 cmd_with_args = UnquoteCommand(cmd);
1959 if (cmd_with_args.empty()) {
1960 impl_->custom_->SetFocus();
1961 wxMessageBoxEx(_("You need to enter a properly quoted command."), _("Cannot set file association"), wxICON_EXCLAMATION);
1962 return;
1963 }
1964 if (!ProgramExists(cmd_with_args.front())) {
1965 impl_->custom_->SetFocus();
1966 wxMessageBoxEx(_("Selected editor does not exist."), _("Cannot set file association"), wxICON_EXCLAMATION, this);
1967 return;
1968 }
1969 cmd = QuoteCommand(cmd_with_args);
1970 impl_->custom_->ChangeValue(cmd);
1971 }
1972 else {
1973 if (def) {
1974 cmd_with_args = GetSystemAssociation(L"foo.txt");
1975 }
1976 else {
1977 cmd_with_args = GetSystemAssociation(file_);
1978 }
1979 if (cmd_with_args.empty()) {
1980 wxMessageBoxEx(_("The associated program could not be found."), _("Cannot set file association"), wxICON_EXCLAMATION, this);
1981 return;
1982 }
1983 }
1984
1985 if (ext_.empty()) {
1986 ext_ = L"/";
1987 }
1988 auto associations = LoadAssociations();
1989 associations[ext_] = cmd_with_args;
1990 SaveAssociations(associations);
1991
1992 EndModal(wxID_OK);
1993}
1994
1995void CNewAssociationDialog::OnBrowseEditor()
1996{
1997 wxFileDialog dlg(this, _("Select default editor"), _T(""), _T(""),
1998#ifdef __WXMSW__
1999 _T("Executable file (*.exe)|*.exe"),
2000#elif __WXMAC__
2001 _T("Applications (*.app)|*.app"),
2002#else
2003 wxFileSelectorDefaultWildcardStr,
2004#endif
2005 wxFD_OPEN | wxFD_FILE_MUST_EXIST);
2006
2007 if (dlg.ShowModal() != wxID_OK) {
2008 return;
2009 }
2010
2011 std::wstring editor = dlg.GetPath().ToStdWstring();
2012 if (editor.empty()) {
2013 return;
2014 }
2015
2016 if (!ProgramExists(editor)) {
2017 impl_->custom_->SetFocus();
2018 wxMessageBoxEx(_("Selected editor does not exist."), _("File not found"), wxICON_EXCLAMATION, this);
2019 return;
2020 }
2021
2022 if (editor.find_first_of(L" \t'\"") != std::wstring::npos) {
2023 fz::replace_substrings(editor, L"\"", L"\"\"");
2024 editor = L"\"" + editor + L"\"";
2025 }
2026
2027 impl_->custom_->ChangeValue(editor);
2028}