Ticket #12307: edithandler.2.cpp

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

Updated version (with filesize estimation and stable file check included)

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 //std::wcout << "SafeCopy from " << from.data() << " to " << to.data() << std::endl;
696 std::ifstream ifile(from.data());
697
698 int iSuccess = 0;
699 for (int i = 0, N = 100; i <= N; i++)
700 {
701 //std::cout << "Autoupload: Trial #" << i << std::endl;
702
703 // Backup file
704 std::ofstream ofile(to.data());
705 ofile << ifile.rdbuf();
706
707 // Reset cursor
708 ifile.clear();
709 ifile.seekg(0, std::ios::beg);
710
711 // Check if file was being written..
712 //std::cout << SameFiles(to.data(), from.data()) << std::endl;
713 if ( SameFiles(to.data(), from.data()) ) iSuccess++;
714 else {
715 if(iSuccess) i = 0;
716 iSuccess = 0;
717 }
718
719 // Check if not modified during at least 1s
720 if(iSuccess > 10) return true;
721 SetTimerState(100);
722 }
723
724 //std::cout << "Failed.." << std::endl;
725 return false;
726}
727
728int CEditHandler::GetSize(const std::wstring &p1)
729{
730 std::ifstream in_file(p1.data(), std::ios::binary);
731 if(!in_file) return -1;
732
733 in_file.seekg(0, std::ios::end);
734 return in_file.tellg();
735}
736
737bool CEditHandler::SameFiles(const std::wstring &p1, const std::wstring &p2)
738{
739 std::ifstream f1(p1.data(), std::ifstream::binary | std::ifstream::ate);
740 std::ifstream f2(p2.data(), std::ifstream::binary | std::ifstream::ate);
741
742 if (f1.fail() || f2.fail())
743 return false; // file problem
744
745 f1.ignore(std::numeric_limits<std::streamsize>::max());
746 f2.ignore(std::numeric_limits<std::streamsize>::max());
747 if(f1.gcount() != f2.gcount())
748 return false; // not same size..
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 return std::equal(std::istreambuf_iterator<char>(f1.rdbuf()),
757 std::istreambuf_iterator<char>(),
758 std::istreambuf_iterator<char>(f2.rdbuf()));
759}
760
761void CEditHandler::CheckForModifications(bool emitEvent)
762{
763 static bool insideCheckForModifications = false;
764 if (insideCheckForModifications) {
765 return;
766 }
767
768 if (emitEvent) {
769 QueueEvent(new wxCommandEvent(fzEDIT_CHANGEDFILE));
770 return;
771 }
772
773 insideCheckForModifications = true;
774
775 for (int i = 0; i < 2; ++i) {
776
777checkmodifications_loopbegin:
778 for (auto iter = m_fileDataList[i].begin(); iter != m_fileDataList[i].end(); ++iter) {
779 if (iter->state != edit) {
780 continue;
781 }
782
783 fz::datetime mtime;
784 bool is_link;
785 if (fz::local_filesys::get_file_info(fz::to_native(iter->localFile), is_link, 0, &mtime, 0) != fz::local_filesys::file) {
786 m_fileDataList[i].erase(iter);
787
788 // Evil goto. Imo the next C++ standard needs a comefrom keyword.
789 goto checkmodifications_loopbegin;
790 }
791
792 if (mtime.empty()) {
793 continue;
794 }
795
796 if (!iter->modificationTime.empty() && !iter->modificationTime.compare(mtime)) {
797 continue;
798 }
799
800 // File has changed, ask user what to do
801 m_busyTimer.Stop();
802 if (!wxDialogEx::CanShowPopupDialog()) {
803 m_busyTimer.Start(1000, true);
804 insideCheckForModifications = false;
805 return;
806 }
807 wxTopLevelWindow* pTopWindow = (wxTopLevelWindow*)wxTheApp->GetTopWindow();
808 if (pTopWindow && pTopWindow->IsIconized()) {
809 pTopWindow->RequestUserAttention(wxUSER_ATTENTION_INFO);
810 insideCheckForModifications = false;
811 return;
812 }
813
814 // Auto-upload: only considering files smaller than "maxsize" (usually text or quick edit files..)
815 int size = GetSize(iter->localFile);
816 int maxsize = 10 * 1024*1024; // 10 MB
817 if(size < maxsize) {
818
819 std::wstring localFileBak = std::wstring(iter->localFile);
820 std::wstring localFileCopy = GetTemporaryFile(std::wstring(iter->localFile + ".bak"));
821 if (! localFileCopy.empty()) {
822
823 localFileBak = iter->localFile;
824 iter->localFile = localFileCopy;
825 }
826
827 bool autoupload = SafeCopy(localFileBak, localFileCopy);
828 if(autoupload) {
829
830 bool remove = false;
831 UploadFile(CEditHandler::fileType(i), iter, remove);
832 std::remove((const char*) localFileCopy.c_str());
833
834 iter->localFile = localFileBak;
835 goto checkmodifications_loopbegin;
836 }
837 }
838
839 // Standard procedure with display notification
840 bool remove = false;
841 int res = DisplayChangeNotification(CEditHandler::fileType(i), *iter, remove);
842 if (res == -1)
843 continue;
844
845 if (res == wxID_YES) {
846 UploadFile(CEditHandler::fileType(i), iter, remove);
847 goto checkmodifications_loopbegin;
848
849 }
850 else if (remove && res != wxID_CANCEL) {
851 if (i == static_cast<int>(remote)) {
852 if (fz::local_filesys::get_file_info(fz::to_native(iter->localFile), is_link, 0, &mtime, 0) != fz::local_filesys::file || wxRemoveFile(iter->localFile)) {
853 m_fileDataList[i].erase(iter);
854 goto checkmodifications_loopbegin;
855 }
856 iter->state = removing;
857 }
858 else {
859 m_fileDataList[i].erase(iter);
860 goto checkmodifications_loopbegin;
861 }
862 }
863 else if (fz::local_filesys::get_file_info(fz::to_native(iter->localFile), is_link, 0, &mtime, 0) != fz::local_filesys::file) {
864 m_fileDataList[i].erase(iter);
865 goto checkmodifications_loopbegin;
866 }
867 else {
868 iter->modificationTime = mtime;
869 }
870 }
871 }
872
873 SetTimerState(100);
874 insideCheckForModifications = false;
875}
876
877int CEditHandler::DisplayChangeNotification(CEditHandler::fileType type, CEditHandler::t_fileData const& data, bool& remove)
878{
879 wxDialogEx dlg;
880
881 if (!dlg.Create(wxTheApp->GetTopWindow(), -1, _("File has changed"))) {
882 return -1;
883 }
884
885 auto& lay = dlg.layout();
886
887 auto main = lay.createMain(&dlg, 1);
888 main->AddGrowableCol(0);
889
890 main->Add(new wxStaticText(&dlg, -1, _("A file previously opened has been changed.")));
891
892 auto inner = lay.createFlex(2);
893 main->Add(inner);
894
895 inner->Add(new wxStaticText(&dlg, -1, _("Filename:")));
896 inner->Add(new wxStaticText(&dlg, -1, LabelEscape((type == local) ? data.localFile : data.remoteFile)));
897
898 if (type == remote) {
899 std::wstring file = data.localFile;
900 size_t pos = file.rfind(wxFileName::GetPathSeparator());
901 if (pos != std::wstring::npos) {
902 file = file.substr(pos + 1);
903 }
904 if (file != data.remoteFile) {
905 inner->Add(new wxStaticText(&dlg, -1, _("Opened as:")));
906 inner->Add(new wxStaticText(&dlg, -1, LabelEscape(file)));
907 }
908 }
909
910 inner->Add(new wxStaticText(&dlg, -1, _("Server:")));
911 inner->Add(new wxStaticText(&dlg, -1, LabelEscape(data.site.Format(ServerFormat::with_user_and_optional_port))));
912
913 inner->Add(new wxStaticText(&dlg, -1, _("Remote path:")));
914 inner->Add(new wxStaticText(&dlg, -1, LabelEscape(data.remotePath.GetPath())));
915
916 main->Add(new wxStaticLine(&dlg), lay.grow);
917
918 wxCheckBox* cb{};
919 if (type == local) {
920 main->Add(new wxStaticText(&dlg, -1, _("Upload this file to the server?")));
921 cb = new wxCheckBox(&dlg, -1, _("&Finish editing"));
922 }
923 else {
924 main->Add(new wxStaticText(&dlg, -1, _("Upload this file back to the server?")));
925 cb = new wxCheckBox(&dlg, -1, _("&Finish editing and delete local file"));
926
927 }
928 main->Add(cb);
929
930 auto buttons = lay.createButtonSizer(&dlg, main, false);
931 auto yes = new wxButton(&dlg, wxID_YES, _("&Yes"));
932 yes->SetDefault();
933 buttons->AddButton(yes);
934 auto no = new wxButton(&dlg, wxID_NO, _("&No"));
935 buttons->AddButton(no);
936 buttons->Realize();
937
938 yes->Bind(wxEVT_BUTTON, [&dlg](wxEvent const&) { dlg.EndDialog(wxID_YES); });
939 no->Bind(wxEVT_BUTTON, [&dlg](wxEvent const&) { dlg.EndDialog(wxID_NO); });
940
941 dlg.Layout();
942 dlg.GetSizer()->Fit(&dlg);
943
944 int res = dlg.ShowModal();
945
946 remove = cb->IsChecked();
947
948 return res;
949}
950
951bool CEditHandler::UploadFile(std::wstring const& file, CServerPath const& remotePath, Site const& site, bool unedit)
952{
953 std::list<t_fileData>::iterator iter = GetFile(file, remotePath, site);
954 return UploadFile(remote, iter, unedit);
955}
956
957bool CEditHandler::UploadFile(std::wstring const& file, bool unedit)
958{
959 std::list<t_fileData>::iterator iter = GetFile(file);
960 return UploadFile(local, iter, unedit);
961}
962
963bool CEditHandler::UploadFile(fileType type, std::list<t_fileData>::iterator iter, bool unedit)
964{
965 wxCHECK(type != none, false);
966
967 if (iter == m_fileDataList[type].end()) {
968 return false;
969 }
970
971 wxASSERT(iter->state == edit || iter->state == upload_and_remove_failed);
972 if (iter->state != edit && iter->state != upload_and_remove_failed) {
973 return false;
974 }
975
976 iter->state = unedit ? upload_and_remove : upload;
977
978 int64_t size;
979 fz::datetime mtime;
980
981 bool is_link;
982 if (fz::local_filesys::get_file_info(fz::to_native(iter->localFile), is_link, &size, &mtime, 0) != fz::local_filesys::file) {
983 m_fileDataList[type].erase(iter);
984 return false;
985 }
986
987 if (mtime.empty()) {
988 mtime = fz::datetime::now();
989 }
990
991 iter->modificationTime = mtime;
992
993 wxASSERT(m_pQueue);
994
995 std::wstring file;
996 CLocalPath localPath(iter->localFile, &file);
997 if (file.empty()) {
998 m_fileDataList[type].erase(iter);
999 return false;
1000 }
1001
1002 m_pQueue->QueueFile(false, false, file, (file == iter->remoteFile) ? std::wstring() : iter->remoteFile, localPath, iter->remotePath, iter->site, size, type, QueuePriority::high);
1003 m_pQueue->QueueFile_Finish(true);
1004
1005 return true;
1006}
1007
1008void CEditHandler::OnTimerEvent(wxTimerEvent&)
1009{
1010#ifdef __WXMSW__
1011 // Don't check for changes if mouse is captured,
1012 // e.g. if user is dragging a file
1013 if (GetCapture()) {
1014 return;
1015 }
1016#endif
1017
1018 CheckForModifications(true);
1019}
1020
1021void CEditHandler::SetTimerState(int t)
1022{
1023 bool editing = GetFileCount(none, edit) != 0;
1024
1025 if (m_timer.IsRunning()) {
1026 if (!editing) {
1027 m_timer.Stop();
1028 }
1029 }
1030 else if (editing) {
1031 m_timer.Start(t);
1032 }
1033}
1034
1035std::vector<std::wstring> CEditHandler::CanOpen(std::wstring const& fileName, bool &program_exists)
1036{
1037 auto cmd_with_args = GetAssociation(fileName);
1038 if (cmd_with_args.empty()) {
1039 return cmd_with_args;
1040 }
1041
1042 program_exists = ProgramExists(cmd_with_args.front());
1043
1044 return cmd_with_args;
1045}
1046
1047std::vector<std::wstring> CEditHandler::GetAssociation(std::wstring const& file)
1048{
1049 std::vector<std::wstring> ret;
1050
1051 if (!COptions::Get()->get_int(OPTION_EDIT_ALWAYSDEFAULT)) {
1052 ret = GetCustomAssociation(file);
1053 }
1054
1055 if (ret.empty()) {
1056 std::wstring command = COptions::Get()->get_string(OPTION_EDIT_DEFAULTEDITOR);
1057 if (!command.empty()) {
1058 if (command[0] == '1') {
1059 // Text editor
1060 ret = GetSystemAssociation(L"foo.txt");
1061 }
1062 else if (command[0] == '2') {
1063 ret = UnquoteCommand(std::wstring_view(command).substr(1));
1064 }
1065 }
1066 }
1067
1068 return ret;
1069}
1070
1071std::vector<std::wstring> CEditHandler::GetCustomAssociation(std::wstring_view const& file)
1072{
1073 std::vector<std::wstring> ret;
1074
1075 std::wstring ext = GetExtension(file);
1076 if (ext.empty()) {
1077 ext = L"/";
1078 }
1079
1080 auto assocs = LoadAssociations();
1081 auto it = assocs.find(ext);
1082 if (it != assocs.end()) {
1083 ret = it->second;
1084 }
1085 return ret;
1086}
1087
1088void CEditHandler::OnChangedFileEvent(wxCommandEvent&)
1089{
1090 CheckForModifications();
1091}
1092
1093std::wstring CEditHandler::GetTemporaryFile(std::wstring name)
1094{
1095 name = CQueueView::ReplaceInvalidCharacters(name, true);
1096#ifdef __WXMSW__
1097 // MAX_PATH - 1 is theoretical limit, we subtract another 4 to allow
1098 // editors which create temporary files
1099 size_t max = MAX_PATH - 5;
1100#else
1101 size_t max = std::wstring::npos;
1102#endif
1103 if (max != std::wstring::npos) {
1104 name = TruncateFilename(m_localDir, name, max);
1105 if (name.empty()) {
1106 return std::wstring();
1107 }
1108 }
1109
1110 std::wstring file = m_localDir + name;
1111 if (!FilenameExists(file)) {
1112 return file;
1113 }
1114
1115 if (max != std::wstring::npos) {
1116 --max;
1117 }
1118 int cutoff = 1;
1119 int n = 1;
1120 while (++n < 10000) { // Just to give up eventually
1121 // Further reduce length if needed
1122 if (max != std::wstring::npos && n >= cutoff) {
1123 cutoff *= 10;
1124 --max;
1125 name = TruncateFilename(m_localDir, name, max);
1126 if (name.empty()) {
1127 return std::wstring();
1128 }
1129 }
1130
1131 size_t pos = name.rfind('.');
1132 if (pos == std::wstring::npos || !pos) {
1133 file = m_localDir + name + fz::sprintf(L" %d", n);
1134 }
1135 else {
1136 file = m_localDir + name.substr(0, pos) + fz::sprintf(L" %d", n) + name.substr(pos);
1137 }
1138
1139 if (!FilenameExists(file)) {
1140 return file;
1141 }
1142 }
1143
1144 return std::wstring();
1145}
1146
1147std::wstring CEditHandler::TruncateFilename(std::wstring const& path, std::wstring const& name, size_t max)
1148{
1149 size_t const pathlen = path.size();
1150 size_t const namelen = name.size();
1151
1152 if (namelen + pathlen > max) {
1153 size_t pos = name.rfind('.');
1154 if (pos != std::wstring::npos) {
1155 size_t extlen = namelen - pos;
1156 if (pathlen + extlen >= max)
1157 {
1158 // Cannot truncate extension
1159 return std::wstring();
1160 }
1161
1162 return name.substr(0, max - pathlen - extlen) + name.substr(pos);
1163 }
1164 }
1165
1166 return name;
1167}
1168
1169bool CEditHandler::FilenameExists(std::wstring const& file)
1170{
1171 for (auto const& fileData : m_fileDataList[remote]) {
1172 // Always ignore case, we don't know which type of filesystem the user profile
1173 // is installed upon.
1174 if (!wxString(fileData.localFile).CmpNoCase(file)) {
1175 return true;
1176 }
1177 }
1178
1179 if (wxFileName::FileExists(file)) {
1180 // Save to remove, it's not marked as edited anymore.
1181 {
1182 wxLogNull log;
1183 wxRemoveFile(file);
1184 }
1185
1186 if (wxFileName::FileExists(file)) {
1187 return true;
1188 }
1189 }
1190
1191 return false;
1192}
1193
1194
1195
1196bool CEditHandler::Edit(CEditHandler::fileType type, std::wstring const& fileName, CServerPath const& path, Site const& site, int64_t size, wxWindow* parent)
1197{
1198 std::vector<FileData> data;
1199 FileData d{fileName, size};
1200 data.push_back(d);
1201
1202 return Edit(type, data, path, site, parent);
1203}
1204
1205bool CEditHandler::Edit(CEditHandler::fileType type, std::vector<FileData> const& data, CServerPath const& path, Site const& site, wxWindow* parent)
1206{
1207 if (type == CEditHandler::remote) {
1208 std::wstring const& localDir = GetLocalDirectory();
1209 if (localDir.empty()) {
1210 wxMessageBoxEx(_("Could not get temporary directory to download file into."), _("Cannot edit file"), wxICON_STOP);
1211 return false;
1212 }
1213 }
1214
1215 if (data.empty()) {
1216 wxBell();
1217 return false;
1218 }
1219
1220 if (data.size() > 10) {
1221 CConditionalDialog dlg(parent, CConditionalDialog::many_selected_for_edit, CConditionalDialog::yesno);
1222 dlg.SetTitle(_("Confirmation needed"));
1223 dlg.AddText(_("You have selected more than 10 files for editing, do you really want to continue?"));
1224
1225 if (!dlg.Run()) {
1226 return false;
1227 }
1228 }
1229
1230 bool success = true;
1231 int already_editing_action{};
1232 for (auto const& file : data) {
1233 if (!DoEdit(type, file, path, site, parent, data.size(), already_editing_action)) {
1234 success = false;
1235 }
1236 }
1237
1238 return success;
1239}
1240
1241bool CEditHandler::DoEdit(CEditHandler::fileType type, FileData const& file, CServerPath const& path, Site const& site, wxWindow* parent, size_t fileCount, int& already_editing_action)
1242{
1243 for (auto const& c : GetExtension(file.name)) {
1244 if (c < 32 && c != '\t') {
1245 wxMessageBoxEx(_("Forbidden character in file extension."), _("Cannot view/edit selected file"), wxICON_EXCLAMATION);
1246 return false;
1247 }
1248 }
1249
1250 // First check whether this file is already being edited
1251 fileState state;
1252 if (type == local) {
1253 state = GetFileState(file.name);
1254 }
1255 else {
1256 state = GetFileState(file.name, path, site);
1257 }
1258 switch (state)
1259 {
1260 case CEditHandler::download:
1261 case CEditHandler::upload:
1262 case CEditHandler::upload_and_remove:
1263 case CEditHandler::upload_and_remove_failed:
1264 wxMessageBoxEx(_("A file with that name is already being transferred."), _("Cannot view/edit selected file"), wxICON_EXCLAMATION);
1265 return false;
1266 case CEditHandler::removing:
1267 if (!Remove(file.name, path, site)) {
1268 wxMessageBoxEx(_("A file with that name is still being edited. Please close it and try again."), _("Selected file is already opened"), wxICON_EXCLAMATION);
1269 return false;
1270 }
1271 break;
1272 case CEditHandler::edit:
1273 {
1274 int action = already_editing_action;
1275 if (!action) {
1276 wxDialogEx dlg;
1277 if (!dlg.Create(parent, -1, _("Selected file already being edited"))) {
1278 wxBell();
1279 return false;
1280 }
1281
1282 auto& lay = dlg.layout();
1283 auto main = lay.createMain(&dlg, 1);
1284 main->AddGrowableCol(0);
1285
1286 main->Add(new wxStaticText(&dlg, -1, _("The selected file is already being edited:")));
1287 main->Add(new wxStaticText(&dlg, -1, LabelEscape(file.name)));
1288
1289 main->AddSpacer(0);
1290
1291 int choices = COptions::Get()->get_int(OPTION_PERSISTENT_CHOICES);
1292
1293 wxRadioButton* reopen{};
1294 if (type == local) {
1295 main->Add(new wxStaticText(&dlg, -1, _("Do you want to reopen this file?")));
1296 }
1297 else {
1298 main->Add(new wxStaticText(&dlg, -1, _("Action to perform:")));
1299
1300 reopen = new wxRadioButton(&dlg, -1, _("&Reopen local file"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP);
1301 main->Add(reopen);
1302 wxRadioButton* retransfer = new wxRadioButton(&dlg, -1, _("&Discard local file then download and edit file anew"));
1303 main->Add(retransfer);
1304
1305 if (choices & edit_choices::edit_existing_action) {
1306 retransfer->SetValue(true);
1307 }
1308 else {
1309 reopen->SetValue(true);
1310 }
1311 }
1312
1313 wxCheckBox* always{};
1314 if (fileCount > 1) {
1315 always = new wxCheckBox(&dlg, -1, _("Do the same with &all selected files already being edited"));
1316 main->Add(always);
1317 if (choices & edit_choices::edit_existing_always) {
1318 always->SetValue(true);
1319 }
1320 }
1321
1322 auto buttons = lay.createButtonSizer(&dlg, main, true);
1323
1324 if (type == remote) {
1325 auto ok = new wxButton(&dlg, wxID_OK, _("OK"));
1326 ok->SetDefault();
1327 buttons->AddButton(ok);
1328 auto cancel = new wxButton(&dlg, wxID_CANCEL, _("Cancel"));
1329 buttons->AddButton(cancel);
1330 }
1331 else {
1332 auto yes = new wxButton(&dlg, wxID_YES, _("&Yes"));
1333 yes->SetDefault();
1334 buttons->AddButton(yes);
1335 auto no = new wxButton(&dlg, wxID_NO, _("&No"));
1336 buttons->AddButton(no);
1337 yes->Bind(wxEVT_BUTTON, [&dlg](wxEvent const&) { dlg.EndDialog(wxID_YES); });
1338 no->Bind(wxEVT_BUTTON, [&dlg](wxEvent const&) { dlg.EndDialog(wxID_NO); });
1339 }
1340 buttons->Realize();
1341
1342 dlg.GetSizer()->Fit(&dlg);
1343 int res = dlg.ShowModal();
1344 if (res != wxID_OK && res != wxID_YES) {
1345 wxBell();
1346 action = -1;
1347 }
1348 else if (type == CEditHandler::local || (reopen && reopen->GetValue())) {
1349 action = 1;
1350 if (type == CEditHandler::remote) {
1351 choices &= ~edit_choices::edit_existing_action;
1352 }
1353 }
1354 else {
1355 action = 2;
1356 choices |= edit_choices::edit_existing_action;
1357 }
1358
1359 if (fileCount > 1) {
1360 if (always && always->GetValue()) {
1361 already_editing_action = action;
1362 choices |= edit_choices::edit_existing_always;
1363 }
1364 else {
1365 choices &= ~edit_choices::edit_existing_always;
1366 }
1367 }
1368 COptions::Get()->set(OPTION_PERSISTENT_CHOICES, choices);
1369 }
1370
1371 if (action == -1) {
1372 return false;
1373 }
1374 else if (action == 1) {
1375 if (type == CEditHandler::local) {
1376 LaunchEditor(file.name);
1377 }
1378 else {
1379 LaunchEditor(file.name, path, site);
1380 }
1381 return true;
1382 }
1383 else {
1384 if (!Remove(file.name, path, site)) {
1385 wxMessageBoxEx(_("The selected file is still opened in some other program, please close it."), _("Selected file is still being edited"), wxICON_EXCLAMATION);
1386 return false;
1387 }
1388 }
1389 }
1390 break;
1391 default:
1392 break;
1393 }
1394
1395 // Create local filename if needed
1396 std::wstring localFile;
1397 std::wstring remoteFile;
1398 if (type == fileType::local) {
1399 localFile = file.name;
1400
1401 CLocalPath localPath(localFile, &remoteFile);
1402 if (localPath.empty()) {
1403 wxBell();
1404 return false;
1405 }
1406 }
1407 else {
1408 localFile = GetTemporaryFile(file.name);
1409 if (localFile.empty()) {
1410 wxMessageBoxEx(_("Could not create temporary file name."), _("Cannot view/edit selected file"), wxICON_EXCLAMATION);
1411 return false;
1412 }
1413 remoteFile = file.name;
1414 }
1415
1416
1417 // Find associated program
1418 bool program_exists = false;
1419 std::vector<std::wstring> cmd_with_args;
1420 if (!wxGetKeyState(WXK_SHIFT) || COptions::Get()->get_int(OPTION_EDIT_ALWAYSDEFAULT)) {
1421 cmd_with_args = CanOpen(file.name, program_exists);
1422 }
1423 if (cmd_with_args.empty()) {
1424 CNewAssociationDialog dlg(parent);
1425 if (!dlg.Run(file.name)) {
1426 return false;
1427 }
1428 cmd_with_args = CanOpen(file.name, program_exists);
1429 if (cmd_with_args.empty()) {
1430 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);
1431 return false;
1432 }
1433 }
1434 if (!program_exists) {
1435 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));
1436 wxMessageBoxEx(msg, _("Cannot edit file"), wxICON_EXCLAMATION);
1437 return false;
1438 }
1439
1440 // We can proceed with adding the item and either open it or transfer it.
1441 return AddFile(type, localFile, remoteFile, path, site, file.size);
1442}
1443
1444
1445#define COLUMN_NAME 0
1446#define COLUMN_TYPE 1
1447#define COLUMN_REMOTEPATH 2
1448#define COLUMN_STATUS 3
1449
1450struct CEditHandlerStatusDialog::impl final
1451{
1452 wxWindow* parent_{};
1453
1454 wxListCtrlEx* listCtrl_{};
1455
1456 wxButton* unedit_{};
1457 wxButton* upload_{};
1458 wxButton* upload_and_unedit_{};
1459 wxButton* open_{};
1460 CEditHandler* editHandler_{};
1461
1462 std::unique_ptr<CWindowStateManager> windowStateManager_;
1463};
1464
1465CEditHandlerStatusDialog::CEditHandlerStatusDialog(wxWindow* parent)
1466 : impl_(std::make_unique<impl>())
1467{
1468 impl_->parent_ = parent;
1469}
1470
1471CEditHandlerStatusDialog::~CEditHandlerStatusDialog()
1472{
1473 if (impl_ && impl_->windowStateManager_) {
1474 impl_->windowStateManager_->Remember(OPTION_EDITSTATUSDIALOG_SIZE);
1475 }
1476}
1477
1478int CEditHandlerStatusDialog::ShowModal()
1479{
1480 impl_->editHandler_ = CEditHandler::Get();
1481 if (!impl_->editHandler_) {
1482 return wxID_CANCEL;
1483 }
1484
1485 if (!impl_->editHandler_->GetFileCount(CEditHandler::none, CEditHandler::unknown)) {
1486 wxMessageBoxEx(_("No files are currently being edited."), _("Cannot show dialog"), wxICON_INFORMATION, impl_->parent_);
1487 return wxID_CANCEL;
1488 }
1489
1490 if (!Create(impl_->parent_, -1, _("Files currently being edited"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)) {
1491 return wxID_CANCEL;
1492 }
1493
1494 auto & lay = layout();
1495 auto main = lay.createMain(this, 1);
1496
1497 main->Add(new wxStaticText(this, -1, _("The &following files are currently being edited:")));
1498
1499 impl_->listCtrl_ = new wxListCtrlEx(this, -1, wxDefaultPosition, wxDefaultSize, wxLC_REPORT);
1500 impl_->listCtrl_->SetFocus();
1501 main->Add(impl_->listCtrl_, lay.grow);
1502 main->AddGrowableCol(0);
1503 main->AddGrowableRow(1);
1504
1505 impl_->listCtrl_->InsertColumn(0, _("Filename"));
1506 impl_->listCtrl_->InsertColumn(1, _("Type"));
1507 impl_->listCtrl_->InsertColumn(2, _("Remote path"));
1508 impl_->listCtrl_->InsertColumn(3, _("Status"));
1509
1510 {
1511 const std::list<CEditHandler::t_fileData>& files = impl_->editHandler_->GetFiles(CEditHandler::remote);
1512 unsigned int i = 0;
1513 for (std::list<CEditHandler::t_fileData>::const_iterator iter = files.begin(); iter != files.end(); ++iter, ++i) {
1514 impl_->listCtrl_->InsertItem(i, iter->remoteFile);
1515 impl_->listCtrl_->SetItem(i, COLUMN_TYPE, _("Remote"));
1516 switch (iter->state)
1517 {
1518 case CEditHandler::download:
1519 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Downloading"));
1520 break;
1521 case CEditHandler::upload:
1522 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Uploading"));
1523 break;
1524 case CEditHandler::upload_and_remove:
1525 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Uploading and pending removal"));
1526 break;
1527 case CEditHandler::upload_and_remove_failed:
1528 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Upload failed"));
1529 break;
1530 case CEditHandler::removing:
1531 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Pending removal"));
1532 break;
1533 case CEditHandler::edit:
1534 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Being edited"));
1535 break;
1536 default:
1537 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Unknown"));
1538 break;
1539 }
1540 impl_->listCtrl_->SetItem(i, COLUMN_REMOTEPATH, iter->site.Format(ServerFormat::with_user_and_optional_port) + iter->remotePath.GetPath());
1541 CEditHandler::t_fileData* pData = new CEditHandler::t_fileData(*iter);
1542 impl_->listCtrl_->SetItemPtrData(i, (wxUIntPtr)pData);
1543 }
1544 }
1545
1546 {
1547 const std::list<CEditHandler::t_fileData>& files = impl_->editHandler_->GetFiles(CEditHandler::local);
1548 unsigned int i = 0;
1549 for (std::list<CEditHandler::t_fileData>::const_iterator iter = files.begin(); iter != files.end(); ++iter, ++i) {
1550 impl_->listCtrl_->InsertItem(i, iter->localFile);
1551 impl_->listCtrl_->SetItem(i, COLUMN_TYPE, _("Local"));
1552 switch (iter->state)
1553 {
1554 case CEditHandler::upload:
1555 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Uploading"));
1556 break;
1557 case CEditHandler::upload_and_remove:
1558 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Uploading and unediting"));
1559 break;
1560 case CEditHandler::edit:
1561 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Being edited"));
1562 break;
1563 default:
1564 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Unknown"));
1565 break;
1566 }
1567 impl_->listCtrl_->SetItem(i, COLUMN_REMOTEPATH, iter->site.Format(ServerFormat::with_user_and_optional_port) + iter->remotePath.GetPath());
1568 CEditHandler::t_fileData* pData = new CEditHandler::t_fileData(*iter);
1569 impl_->listCtrl_->SetItemPtrData(i, (wxUIntPtr)pData);
1570 }
1571 }
1572
1573 for (int i = 0; i < 4; ++i) {
1574 impl_->listCtrl_->SetColumnWidth(i, wxLIST_AUTOSIZE);
1575 }
1576 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()));
1577
1578 auto onsel = [this](wxListEvent const&) { SetCtrlState(); };
1579 impl_->listCtrl_->Bind(wxEVT_LIST_ITEM_SELECTED, onsel);
1580 impl_->listCtrl_->Bind(wxEVT_LIST_ITEM_DESELECTED, onsel);
1581
1582 main->Add(new wxStaticText(this, -1, _("Action on selected file:")));
1583
1584 auto inner = lay.createGrid(2, 2);
1585 main->Add(inner, lay.halign);
1586
1587 impl_->unedit_ = new wxButton(this, -1, _("&Unedit"));
1588 impl_->unedit_->Bind(wxEVT_BUTTON, [this](wxCommandEvent const&) { OnUnedit(); });
1589 inner->Add(impl_->unedit_, lay.valigng);
1590 impl_->upload_ = new wxButton(this, -1, _("U&pload"));
1591 impl_->upload_->Bind(wxEVT_BUTTON, [this](wxCommandEvent const&) { OnUpload(false); });
1592 inner->Add(impl_->upload_, lay.valigng);
1593 impl_->upload_and_unedit_ = new wxButton(this, -1, _("Up&load and unedit"));
1594 impl_->upload_and_unedit_->Bind(wxEVT_BUTTON, [this](wxCommandEvent const&) { OnUpload(true); });
1595 inner->Add(impl_->upload_and_unedit_, lay.valigng);
1596 impl_->open_ = new wxButton(this, -1, _("Op&en file"));
1597 impl_->open_->Bind(wxEVT_BUTTON, [this](wxCommandEvent const&) { OnEdit(); });
1598 inner->Add(impl_->open_, lay.valigng);
1599
1600
1601 auto buttons = lay.createButtonSizer(this, main, true);
1602 auto ok = new wxButton(this, wxID_OK, _("OK"));
1603 ok->SetDefault();
1604 buttons->AddButton(ok);
1605 buttons->Realize();
1606
1607 GetSizer()->Fit(this);
1608 SetMinClientSize(GetSizer()->GetMinSize());
1609
1610 impl_->windowStateManager_ = std::make_unique<CWindowStateManager>(static_cast<wxTopLevelWindow*>(this));
1611 impl_->windowStateManager_->Restore(OPTION_EDITSTATUSDIALOG_SIZE, GetSize());
1612
1613 SetCtrlState();
1614
1615 int res = wxDialogEx::ShowModal();
1616
1617 for (int i = 0; i < impl_->listCtrl_->GetItemCount(); ++i) {
1618 delete (CEditHandler::t_fileData*)impl_->listCtrl_->GetItemData(i);
1619 }
1620
1621 return res;
1622}
1623
1624void CEditHandlerStatusDialog::SetCtrlState()
1625{
1626 bool selectedEdited = false;
1627 bool selectedOther = false;
1628 bool selectedUploadRemoveFailed = false;
1629
1630 int item = -1;
1631 while ((item = impl_->listCtrl_->GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)) != -1) {
1632 CEditHandler::fileType type;
1633 CEditHandler::t_fileData* pData = GetDataFromItem(item, type);
1634 if (pData->state == CEditHandler::edit) {
1635 selectedEdited = true;
1636 }
1637 else if (pData->state == CEditHandler::upload_and_remove_failed) {
1638 selectedUploadRemoveFailed = true;
1639 }
1640 else {
1641 selectedOther = true;
1642 }
1643 }
1644
1645 bool const select = selectedEdited && !selectedOther && !selectedUploadRemoveFailed;
1646 impl_->unedit_->Enable(select || (!selectedOther && selectedUploadRemoveFailed));
1647 impl_->upload_->Enable(select || (!selectedEdited && !selectedOther && selectedUploadRemoveFailed));
1648 impl_->upload_and_unedit_->Enable(select || (!selectedEdited && !selectedOther && selectedUploadRemoveFailed));
1649 impl_->open_->Enable(select);
1650}
1651
1652void CEditHandlerStatusDialog::OnUnedit()
1653{
1654 std::list<int> files;
1655 int item = -1;
1656 while ((item = impl_->listCtrl_->GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)) != -1) {
1657 impl_->listCtrl_->SetItemState(item, 0, wxLIST_STATE_SELECTED);
1658 CEditHandler::fileType type;
1659 CEditHandler::t_fileData* pData = GetDataFromItem(item, type);
1660 if (pData->state != CEditHandler::edit && pData->state != CEditHandler::upload_and_remove_failed) {
1661 wxBell();
1662 return;
1663 }
1664
1665 files.push_front(item);
1666 }
1667
1668 for (std::list<int>::const_iterator iter = files.begin(); iter != files.end(); ++iter) {
1669 const int i = *iter;
1670
1671 CEditHandler::fileType type;
1672 CEditHandler::t_fileData* pData = GetDataFromItem(i, type);
1673
1674 if (type == CEditHandler::local) {
1675 impl_->editHandler_->Remove(pData->localFile);
1676 delete pData;
1677 impl_->listCtrl_->DeleteItem(i);
1678 }
1679 else {
1680 if (impl_->editHandler_->Remove(pData->remoteFile, pData->remotePath, pData->site)) {
1681 delete pData;
1682 impl_->listCtrl_->DeleteItem(i);
1683 }
1684 else {
1685 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Pending removal"));
1686 }
1687 }
1688 }
1689
1690 SetCtrlState();
1691}
1692
1693void CEditHandlerStatusDialog::OnUpload(bool unedit_after)
1694{
1695 std::list<int> files;
1696 int item = -1;
1697 while ((item = impl_->listCtrl_->GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)) != -1) {
1698 impl_->listCtrl_->SetItemState(item, 0, wxLIST_STATE_SELECTED);
1699
1700 CEditHandler::fileType type;
1701 CEditHandler::t_fileData* pData = GetDataFromItem(item, type);
1702
1703 if (pData->state != CEditHandler::edit && pData->state != CEditHandler::upload_and_remove_failed) {
1704 wxBell();
1705 return;
1706 }
1707 files.push_front(item);
1708 }
1709
1710 for (std::list<int>::const_iterator iter = files.begin(); iter != files.end(); ++iter) {
1711 const int i = *iter;
1712
1713 CEditHandler::fileType type;
1714 CEditHandler::t_fileData* pData = GetDataFromItem(i, type);
1715
1716 bool unedit = unedit_after || pData->state == CEditHandler::upload_and_remove_failed;
1717
1718 if (type == CEditHandler::local) {
1719 impl_->editHandler_->UploadFile(pData->localFile, unedit);
1720 }
1721 else {
1722 impl_->editHandler_->UploadFile(pData->remoteFile, pData->remotePath, pData->site, unedit);
1723 }
1724
1725 if (!unedit) {
1726 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Uploading"));
1727 }
1728 else if (type == CEditHandler::remote) {
1729 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Uploading and pending removal"));
1730 }
1731 else {
1732 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Uploading and unediting"));
1733 }
1734 }
1735
1736 SetCtrlState();
1737}
1738
1739void CEditHandlerStatusDialog::OnEdit()
1740{
1741 std::list<int> files;
1742 int item = -1;
1743 while ((item = impl_->listCtrl_->GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)) != -1) {
1744 impl_->listCtrl_->SetItemState(item, 0, wxLIST_STATE_SELECTED);
1745
1746 CEditHandler::fileType type;
1747 CEditHandler::t_fileData* pData = GetDataFromItem(item, type);
1748
1749 if (pData->state != CEditHandler::edit) {
1750 wxBell();
1751 return;
1752 }
1753 files.push_front(item);
1754 }
1755
1756 for (std::list<int>::const_iterator iter = files.begin(); iter != files.end(); ++iter) {
1757 const int i = *iter;
1758
1759 CEditHandler::fileType type;
1760 CEditHandler::t_fileData* pData = GetDataFromItem(i, type);
1761
1762 if (type == CEditHandler::local) {
1763 if (!impl_->editHandler_->LaunchEditor(pData->localFile)) {
1764 if (impl_->editHandler_->Remove(pData->localFile)) {
1765 delete pData;
1766 impl_->listCtrl_->DeleteItem(i);
1767 }
1768 else {
1769 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Pending removal"));
1770 }
1771 }
1772 }
1773 else {
1774 if (!impl_->editHandler_->LaunchEditor(pData->remoteFile, pData->remotePath, pData->site)) {
1775 if (impl_->editHandler_->Remove(pData->remoteFile, pData->remotePath, pData->site)) {
1776 delete pData;
1777 impl_->listCtrl_->DeleteItem(i);
1778 }
1779 else {
1780 impl_->listCtrl_->SetItem(i, COLUMN_STATUS, _("Pending removal"));
1781 }
1782 }
1783 }
1784 }
1785
1786 SetCtrlState();
1787}
1788
1789CEditHandler::t_fileData* CEditHandlerStatusDialog::GetDataFromItem(int item, CEditHandler::fileType &type)
1790{
1791 CEditHandler::t_fileData* pData = (CEditHandler::t_fileData*)impl_->listCtrl_->GetItemData(item);
1792 wxASSERT(pData);
1793
1794 wxListItem info;
1795 info.SetMask(wxLIST_MASK_TEXT);
1796 info.SetId(item);
1797 info.SetColumn(1);
1798 impl_->listCtrl_->GetItem(info);
1799 if (info.GetText() == _("Local")) {
1800 type = CEditHandler::local;
1801 }
1802 else {
1803 type = CEditHandler::remote;
1804 }
1805
1806 return pData;
1807}
1808
1809
1810
1811struct CNewAssociationDialog::impl
1812{
1813 wxRadioButton* rbSystem_{};
1814 wxRadioButton* rbDefault_{};
1815 wxRadioButton* rbCustom_{};
1816
1817 wxCheckBox* always_{};
1818
1819 wxTextCtrlEx* custom_{};
1820 wxButton* browse_{};
1821};
1822
1823CNewAssociationDialog::CNewAssociationDialog(wxWindow *parent)
1824 : parent_(parent)
1825{
1826}
1827
1828CNewAssociationDialog::~CNewAssociationDialog()
1829{
1830}
1831
1832void ShowQuotingRules(wxWindow* parent);
1833
1834bool CNewAssociationDialog::Run(std::wstring const& file)
1835{
1836 file_ = file;
1837
1838 ext_ = GetExtension(file);
1839
1840 impl_ = std::make_unique<impl>();
1841
1842 Create(parent_, -1, _("No program associated with filetype"));
1843
1844 auto & lay = layout();
1845
1846 auto * main = lay.createMain(this, 1);
1847
1848 if (ext_.empty()) {
1849 main->Add(new wxStaticText(this, -1, _("No program has been associated to edit extensionless files.")));
1850 }
1851 else if (ext_ == L".") {
1852 main->Add(new wxStaticText(this, -1, _("No program has been associated to edit dotfiles.")));
1853 }
1854 else {
1855 main->Add(new wxStaticText(this, -1, wxString::Format(_("No program has been associated to edit files with the extension '%s'."), LabelEscape(ext_))));
1856 }
1857
1858 main->Add(new wxStaticText(this, -1, _("Select how these files should be opened.")));
1859
1860 {
1861 auto const cmd_with_args = GetSystemAssociation(file);
1862 if (!cmd_with_args.empty()) {
1863 impl_->rbSystem_ = new wxRadioButton(this, -1, _("Use system association"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP);
1864 impl_->rbSystem_->Bind(wxEVT_RADIOBUTTON, [this](wxEvent const&) { SetCtrlState(); });
1865 impl_->rbSystem_->SetValue(true);
1866 main->Add(impl_->rbSystem_);
1867 main->Add(new wxStaticText(this, -1, _("The default editor for this file type is:") + L" " + LabelEscape(QuoteCommand(cmd_with_args))), 0, wxLEFT, lay.indent);
1868 }
1869 }
1870
1871 {
1872 auto const cmd_with_args = GetSystemAssociation(L"foo.txt");
1873 if (!cmd_with_args.empty()) {
1874 impl_->rbDefault_ = new wxRadioButton(this, -1, _("Use &default editor for text files"), wxDefaultPosition, wxDefaultSize, impl_->rbSystem_ ? 0 : wxRB_GROUP);
1875 impl_->rbDefault_->Bind(wxEVT_RADIOBUTTON, [this](wxEvent const&) { SetCtrlState(); });
1876 if (!impl_->rbSystem_) {
1877 impl_->rbDefault_->SetValue(true);
1878 }
1879 main->Add(impl_->rbDefault_);
1880 main->Add(new wxStaticText(this, -1, _("The default editor for text files is:") + " " + LabelEscape(QuoteCommand(cmd_with_args))), 0, wxLEFT, lay.indent);
1881 impl_->always_ = new wxCheckBox(this, -1, _("&Always use selection for all unassociated files"));
1882 main->Add(impl_->always_, 0, wxLEFT, lay.indent);
1883 }
1884 }
1885
1886 impl_->rbCustom_ = new wxRadioButton(this, -1, _("&Use custom program"), wxDefaultPosition, wxDefaultSize, (impl_->rbSystem_ || impl_->rbDefault_) ? 0 : wxRB_GROUP);
1887 impl_->rbCustom_->Bind(wxEVT_RADIOBUTTON, [this](wxEvent const&) { SetCtrlState(); });
1888 if (!impl_->rbSystem_ && !impl_->rbDefault_) {
1889 impl_->rbCustom_->SetValue(true);
1890 }
1891 main->Add(impl_->rbCustom_);
1892 auto row = lay.createFlex(2);
1893 row->AddGrowableCol(0);
1894 main->Add(row, 0, wxLEFT|wxGROW, lay.indent);
1895
1896 auto rules = new wxHyperlinkCtrl(this, -1, _("Quoting rules"), wxString());
1897 main->Add(rules, 0, wxLEFT, lay.indent);
1898 rules->Bind(wxEVT_HYPERLINK, [this](wxHyperlinkEvent const&) { ShowQuotingRules(this); });
1899
1900
1901 impl_->custom_ = new wxTextCtrlEx(this, -1, wxString());
1902 row->Add(impl_->custom_, lay.valigng);
1903 impl_->browse_ = new wxButton(this, -1, _("&Browse..."));
1904 impl_->browse_->Bind(wxEVT_BUTTON, [this](wxEvent const&) { OnBrowseEditor(); });
1905 row->Add(impl_->browse_, lay.valign);
1906
1907 auto buttons = lay.createButtonSizer(this, main, true);
1908 auto ok = new wxButton(this, wxID_OK, _("OK"));
1909 ok->SetDefault();
1910 buttons->AddButton(ok);
1911 auto cancel = new wxButton(this, wxID_CANCEL, _("Cancel"));
1912 buttons->AddButton(cancel);
1913 buttons->Realize();
1914
1915 ok->Bind(wxEVT_BUTTON, [this](wxEvent const&) { OnOK(); });
1916
1917 Layout();
1918 GetSizer()->Fit(this);
1919
1920 SetCtrlState();
1921
1922 return ShowModal() == wxID_OK;
1923}
1924
1925void CNewAssociationDialog::SetCtrlState()
1926{
1927 if (impl_->custom_) {
1928 impl_->custom_->Enable(impl_->rbCustom_ && impl_->rbCustom_->GetValue());
1929 }
1930 if (impl_->browse_) {
1931 impl_->browse_->Enable(impl_->rbCustom_ && impl_->rbCustom_->GetValue());
1932 }
1933 if (impl_->always_) {
1934 impl_->always_->Enable(impl_->rbDefault_ && impl_->rbDefault_->GetValue());
1935 }
1936}
1937
1938void CNewAssociationDialog::OnOK()
1939{
1940 const bool custom = impl_->rbCustom_ && impl_->rbCustom_->GetValue();
1941 const bool def = impl_->rbDefault_ && impl_->rbDefault_->GetValue();
1942 const bool always = impl_->always_ && impl_->always_->GetValue();
1943
1944 if (def && always) {
1945 COptions::Get()->set(OPTION_EDIT_DEFAULTEDITOR, _T("1"));
1946 EndModal(wxID_OK);
1947
1948 return;
1949 }
1950
1951 std::vector<std::wstring> cmd_with_args;
1952 if (custom) {
1953 std::wstring cmd = impl_->custom_->GetValue().ToStdWstring();
1954 cmd_with_args = UnquoteCommand(cmd);
1955 if (cmd_with_args.empty()) {
1956 impl_->custom_->SetFocus();
1957 wxMessageBoxEx(_("You need to enter a properly quoted command."), _("Cannot set file association"), wxICON_EXCLAMATION);
1958 return;
1959 }
1960 if (!ProgramExists(cmd_with_args.front())) {
1961 impl_->custom_->SetFocus();
1962 wxMessageBoxEx(_("Selected editor does not exist."), _("Cannot set file association"), wxICON_EXCLAMATION, this);
1963 return;
1964 }
1965 cmd = QuoteCommand(cmd_with_args);
1966 impl_->custom_->ChangeValue(cmd);
1967 }
1968 else {
1969 if (def) {
1970 cmd_with_args = GetSystemAssociation(L"foo.txt");
1971 }
1972 else {
1973 cmd_with_args = GetSystemAssociation(file_);
1974 }
1975 if (cmd_with_args.empty()) {
1976 wxMessageBoxEx(_("The associated program could not be found."), _("Cannot set file association"), wxICON_EXCLAMATION, this);
1977 return;
1978 }
1979 }
1980
1981 if (ext_.empty()) {
1982 ext_ = L"/";
1983 }
1984 auto associations = LoadAssociations();
1985 associations[ext_] = cmd_with_args;
1986 SaveAssociations(associations);
1987
1988 EndModal(wxID_OK);
1989}
1990
1991void CNewAssociationDialog::OnBrowseEditor()
1992{
1993 wxFileDialog dlg(this, _("Select default editor"), _T(""), _T(""),
1994#ifdef __WXMSW__
1995 _T("Executable file (*.exe)|*.exe"),
1996#elif __WXMAC__
1997 _T("Applications (*.app)|*.app"),
1998#else
1999 wxFileSelectorDefaultWildcardStr,
2000#endif
2001 wxFD_OPEN | wxFD_FILE_MUST_EXIST);
2002
2003 if (dlg.ShowModal() != wxID_OK) {
2004 return;
2005 }
2006
2007 std::wstring editor = dlg.GetPath().ToStdWstring();
2008 if (editor.empty()) {
2009 return;
2010 }
2011
2012 if (!ProgramExists(editor)) {
2013 impl_->custom_->SetFocus();
2014 wxMessageBoxEx(_("Selected editor does not exist."), _("File not found"), wxICON_EXCLAMATION, this);
2015 return;
2016 }
2017
2018 if (editor.find_first_of(L" \t'\"") != std::wstring::npos) {
2019 fz::replace_substrings(editor, L"\"", L"\"\"");
2020 editor = L"\"" + editor + L"\"";
2021 }
2022
2023 impl_->custom_->ChangeValue(editor);
2024}