root/branches/mbutscher/work/lib/pwiki/FileCleanup.py @ 263

Revision 263, 52.8 kB (checked in by mbutscher, 2 years ago)

branches/stable-2.1:
* Support for URL appendix "prnr" to create a relative link which is

not relocated (modified) when exported as HTML to a different
destination (backported to 2.1 to provide some backward compatibility
to old behavior)

* Bug fixed: Favorite wiki icons may open wrong wiki
* Bug fixed: Misleading error message and bad error handling for

corrupted wiki config file

* Internal: Default maximum length of compatible filename reduced from

250 to 120

branches/mbutscher/work:
* Less jumping around of selection in doc structure window when adding

text (thanks to Christian Ziemski)

* Support for spaces in bracketed URLs
* Option to control type of URL (bracketed or not) on drag&drop
* Support for URL appendix "prnr" to create a relative link which is

not relocated (modified) when exported as HTML to a different
destination

* Option to sort list in "Open Wiki Word" dialog in reverse

alphabetical order

* Shortcuts introduced to move one or more selected logical lines one

line up or down

* Bug fixed: Favorite wiki icons may open wrong wiki
* Bug fixed: Misleading error message and bad error handling for

corrupted wiki config file

* Internal: Deprecated makeRelUrlAbsolute() and makeAbsPathRelUrl() in

PersonalWikiFrame?, call functions in WikiDocument? instead

* Internal: Default maximum length of compatible filename reduced from

250 to 120

* File cleanup now in usable state (orphaned files complete, missing

files need work yet)

Line 
1"""
2"""
3import os, os.path, traceback, sqlite3
4
5import wx, wx.xrc
6
7from wxHelper import GUI_ID, wxKeyFunctionSink, XrcControls, \
8        runDialogModalFactory
9
10# from .wxHelper import XrcControls
11#
12# import Consts
13
14from .OsAbstract import normalizePath, deleteFile
15from . import StringOps, WindowLayout
16
17from EnhancedGrid import EnhancedGrid
18
19from .ConnectWrapPysqlite import ConnectWrapSyncCommit
20from .DocPages import AliasWikiPage
21
22
23
24class InfoDatabase:
25    def __init__(self, mainControl):
26        self.tempDb = None
27        self.mainControl = mainControl
28        self.wikiDocument = mainControl.getWikiDocument()
29
30        self.fileStorDir = self.wikiDocument.getFileStorage().getStoragePath()
31        self.normFileStorDir = normalizePath(self.fileStorDir)
32
33
34    def _createDatabase(self):
35        """
36        Create empty database with needed scheme.
37        """
38
39#         tempDb = ConnectWrapSyncCommit(sqlite3.connect(ur"C:\Daten\Projekte\Wikidpad\Current\fmtest.sli"))
40        tempDb = ConnectWrapSyncCommit(sqlite3.connect(ur""))
41        # Items (files and directories) found in file storage
42        tempDb.execSql("create table fStorItems("
43                "id integer primary key not null, "
44                "procCounter integer not null, " # Just a counter to keep the order in which items where processed first
45                "fullpath text not null default '', " # absolute path to item (as user would expect it)
46                "normpath text not null default '', " # absolute normalized path to item (also lower-cased for Windows)
47                "relpath text not null default '', " # relative path from root of file storage (the "files" directory)
48                "type integer not null default 0, " # 0: file, 1: directory
49                "deepness integer not null, " # root dir. has 0, everything else has <deepness of container>+1
50                "containerId integer not null default -1, " # id of containing directory or -1 if root
51                "directref integer not null default 0," # !=0: referenced directly
52                "downwardref integer not null default 0," # !=0: inferred reference downwards
53                "upwardref integer not null default 0," # !=0: inferred reference upwards
54                "action integer not null default 0," # action to do (set by user during dialog)
55                "isdefaultaction integer not null default 0," # !=0:  action  is default and not set especially for this item
56                "calcaction integer not null default 0, " # calculated action
57                "errormsg text not null default '' " # error message (created during dialog)
58                        # (inferred and indirectly set by user during dialog)
59                ");")
60
61
62        # Items (files and directories) referenced on wiki pages
63        tempDb.execSql("create table refedItems("
64                "id integer primary key not null, "
65                "fullpath text not null default ''," # absolute path to item (as user would expect it)
66                "normpath text not null default ''," # absolute normalized path to item (also lower-cased for Windows)
67                "infstore integer not null default 0," # !=0: path lies in file store (but item doesn't need to exist)
68                "present integer not null default 0," # !=0: item exists
69                "action integer not null default 0" # action to do (set by user during dialog)
70#                 "fstoreId integer not null default -1" # id of item in  fstoritem  or -1 if outside of f.store or not present at all
71                ");")
72
73
74        # Map from unifiedNames of the wiki pages to the  refeditems  they reference
75        tempDb.execSql("create table unifNameToItem("
76                "unifName text not null default '',"  # unified name of the wiki page
77                "presentationUrl text not null default '',"  # URL as it is written on wiki page
78                "refedItemId integer not null default 0," # id in  refeditems
79                "relative integer not null default 0," # !=0: "rel://" URL, ==0: "file:" URL
80                "tokenPos integer not null default -1," # character position of the token containing the link
81                "tokenLength integer not null default -1," # character length of the token
82                "corePos integer not null default -1," # character position of the link core
83                        # (actual URL without surrounding characters)
84                "coreLength integer not null default -1" # character length of link core
85                ");")
86
87
88#                 "constraint depgraphpk primary key (unifName, refeditemId)"
89
90        tempDb.commit()
91
92        self.tempDb = tempDb
93
94    def getSqlDb(self):
95        return self.tempDb
96
97
98    def _scanFileStore(self):
99        assert self.tempDb
100
101        if not os.path.isdir(self.fileStorDir):
102            # No file storage present
103            return
104
105        self.tempDb.execSql("insert into fStorItems(procCounter, fullpath, "
106                "normpath, relpath, type, containerId, deepness) "
107                "values(0, ?, ?, '', 1, -1, 0)",
108                        (self.fileStorDir, self.normFileStorDir))
109        procCounter = 1
110
111        # Using a stack instead of recursion to avoid hitting rec. limit
112        dirStack = [(self.fileStorDir, self.tempDb.lastrowid, u"", 0)]
113
114        while dirStack:
115            procDir, procDirId, relPath, dirDeepness = dirStack.pop()
116
117            items = os.listdir(procDir)
118#             fpItems = (os.path.join(procDir, n) for n in items)
119
120            for fn in items:
121                fpi = os.path.join(procDir, fn)
122                relFpi = os.path.join(relPath, fn)
123
124                if os.path.isfile(StringOps.longPathEnc(fpi)):
125                    self.tempDb.execSql("insert into fStorItems(procCounter, "
126                            "fullpath, normpath, relpath, type, containerId, "
127                            "deepness) values(?, ?, ?, ?, 0, ?, ?)",
128                            (procCounter, fpi, normalizePath(fpi), relFpi,
129                            procDirId, dirDeepness + 1))
130                    procCounter += 1
131                elif os.path.isdir(StringOps.longPathEnc(fpi)) \
132                        and not os.path.islink(StringOps.longPathEnc(fpi)):
133                    self.tempDb.execSql("insert into fStorItems(procCounter, "
134                            "fullpath, normpath, relpath, type, containerId, "
135                            "deepness) values(?, ?, ?, ?, 1, ?, ?)",
136                            (procCounter, fpi, normalizePath(fpi), relFpi,
137                            procDirId, dirDeepness + 1))
138                    procCounter += 1
139                    dirStack.append((fpi, self.tempDb.lastrowid, relFpi,
140                            dirDeepness + 1))
141
142
143    def updateWikiPage(self, wikiPage):
144        pageAst = wikiPage.getLivePageAst()
145
146        self.tempDb.execSql("delete from unifNameToItem where "
147                "unifName == ?", (wikiPage.getUnifiedPageName(),))
148
149        for urlNode in pageAst.iterDeepByName("urlLink"):
150            url = urlNode.url
151
152            if url.startswith(u"rel://"):
153                url = self.wikiDocument.makeRelUrlAbsolute(url)
154                isRel = 1
155            else:
156                isRel = 0
157
158            if url.startswith(u"file:"):
159                path = StringOps.pathnameFromUrl(url)
160                npath = normalizePath(path)
161                pathId = self.tempDb.execSqlQuerySingleItem(
162                        "select id from refedItems where normpath=?",
163                        (npath,))
164                if pathId is None:
165                    # Create new item
166                    lpe = StringOps.longPathEnc(path)
167                    pathEx = os.path.isfile(lpe) \
168                            or (os.path.isdir(lpe)
169                            and not os.path.islink(lpe))
170                    pathEx = 1 if pathEx else 0
171
172                    inFileStor = self.normFileStorDir == npath or \
173                            StringOps.testContainedInDir(self.normFileStorDir,
174                            npath)
175                    inFileStor = 1 if inFileStor else 0
176
177                    self.tempDb.execSql("insert into refedItems("
178                            "fullpath, normpath, infstore, present) "
179                            "values(?, ?, ?, ?)",
180                            (path, npath, inFileStor, pathEx))
181
182                    pathId = self.tempDb.lastrowid
183
184                self.tempDb.execSql("insert or replace into unifNameToItem("
185                        "unifName, presentationUrl, refedItemId, relative,"
186                        "tokenPos, tokenLength, corePos, coreLength) "
187                        "values (?, ?, ?, ?, ?, ?, ?, ?)",
188                        (wikiPage.getUnifiedPageName(),
189                        urlNode.coreNode.getString(), pathId, isRel,
190                        urlNode.pos, urlNode.strLength,
191                        urlNode.coreNode.pos,
192                        urlNode.coreNode.strLength))
193
194
195    def _scanLinks(self, progresshandler):
196        wikiWords = self.wikiDocument.getWikiData().getAllDefinedWikiPageNames()
197
198        progresshandler.open(len(wikiWords) + 1)
199        try:
200            step = 1
201
202            for wikiWord in wikiWords:
203                progresshandler.update(step, _(u"Scan links in %s") % wikiWord)
204
205                wikiPage = self.wikiDocument._getWikiPageNoErrorNoCache(wikiWord)
206                if isinstance(wikiPage, AliasWikiPage):
207                    # This should never be an alias page
208                    # This can only happen if there is a real page with
209                    # the same name as an alias
210                    continue  # TODO: Better solution
211
212                self.updateWikiPage(wikiPage)
213#                 pageAst = wikiPage.getLivePageAst()
214#
215#                 for urlNode in pageAst.iterDeepByName("urlLink"):
216#                     url = urlNode.url
217#
218#                     if url.startswith(u"rel://"):
219#                         url = self.wikiDocument.makeRelUrlAbsolute(url)
220#                         isRel = 1
221#                     else:
222#                         isRel = 0
223#
224#                     if url.startswith(u"file:"):
225#                         path = StringOps.pathnameFromUrl(url)
226#                         npath = normalizePath(path)
227#                         pathId = self.tempDb.execSqlQuerySingleItem(
228#                                 "select id from refedItems where normpath=?",
229#                                 (npath,))
230#                         if pathId is None:
231#                             # Create new item
232#                             lpe = StringOps.longPathEnc(path)
233#                             pathEx = os.path.isfile(lpe) \
234#                                     or (os.path.isdir(lpe)
235#                                     and not os.path.islink(lpe))
236#                             pathEx = 1 if pathEx else 0
237#
238#                             inFileStor = self.normFileStorDir == npath or \
239#                                     StringOps.testContainedInDir(self.normFileStorDir,
240#                                     npath)
241#                             inFileStor = 1 if inFileStor else 0
242#
243#                             self.tempDb.execSql("insert into refedItems("
244#                                     "fullpath, normpath, infstore, present) "
245#                                     "values(?, ?, ?, ?)",
246#                                     (path, npath, inFileStor, pathEx))
247#
248#                             pathId = self.tempDb.lastrowid
249#
250#                         self.tempDb.execSql("insert or replace into unifNameToItem("
251#                                 "unifName, presentationUrl, refedItemId, relative,"
252#                                 "tokenPos, tokenLength, corePos, coreLength) "
253#                                 "values (?, ?, ?, ?, ?, ?, ?, ?)",
254#                                 (wikiPage.getUnifiedPageName(),
255#                                 urlNode.coreNode.getString(), pathId, isRel,
256#                                 urlNode.pos, urlNode.strLength,
257#                                 urlNode.coreNode.pos,
258#                                 urlNode.coreNode.strLength))
259
260                step += 1
261
262        finally:
263            progresshandler.close()
264
265
266    def _markDirectlyReferencedInFileStorage(self):
267        """
268        If a path is present in refedItems and fStorItems then
269        fStorItems.directref should be set 1
270        """
271
272        self.tempDb.execSql("update fStorItems set directref = 1 where "
273                "normpath in (select normpath from refedItems)")
274
275
276    def _inferUpwardRefInFileStorage(self):
277        """
278        If an item has a direct or upward reference then its containing directory
279        should also have an upward reference i.e. if a file is referenced its
280        containing directory must be kept, too.
281        """
282        # This should actually be done recursively. Therefore statement
283        # is repeated until no more rows are changed
284
285        self.tempDb.execSqlUntilNoChange("update fStorItems set upwardref = 1 where "
286                "upwardref == 0 and id in (select containerId from fStorItems "
287                "where directref != 0 or upwardref != 0)")
288
289
290    def _inferDownwardRefInFileStorage(self):
291        """
292        If a directory has a direct or downward reference then its contained
293        items also get a downward reference. The idea is that if the user
294        references a particular directory she probably also wants to keep
295        the files and directories in it even if they aren't referenced.
296
297        This is optional. Maybe there will be an option later to
298        run this only to a given deepness.
299        """
300        # This should actually be done recursively. Therefore statement
301        # is repeated until no more rows are changed
302
303        self.tempDb.execSqlUntilNoChange("update fStorItems set downwardref = 1 where "
304                "downwardref == 0 and containerId in "
305                "(select id from fStorItems where "
306                "type == 1 and (directref != 0 or downwardref != 0))")
307
308
309    def deleteUninteresting(self):
310        """
311        Remove all rows for items which exist and are referenced.
312        """
313        self.tempDb.execSql("delete from fStorItems where "
314                "(directref or downwardref or upwardref)")
315        self.tempDb.execSql("delete from refedItems where present")
316        self.tempDb.execSql("delete from unifNameToItem where "
317                "refedItemId not in (select id from refedItems)")
318
319
320    def buildDatabaseBeforeDialog(self, progresshandler, options):
321        self._createDatabase()
322        self._scanFileStore()
323        self._scanLinks(progresshandler)
324        self._markDirectlyReferencedInFileStorage()
325        self._inferUpwardRefInFileStorage()
326        if options["downwardRef"]:
327            self._inferDownwardRefInFileStorage()
328        self.deleteUninteresting()
329
330        self.tempDb.commit()
331
332
333    def calcActionAndErrorsDuringDialog(self, orphanedActionDefault,
334            orphanedDownwardRef):
335
336        CALCACTION_KEEP = 1
337        CALCACTION_DELETE = 2
338        CALCACTION_COLLECT = 5  # 1|4
339
340
341        MASK_KEEP = 1
342        MASK_COLLECT = 4
343#         MASK_DEFAULT = 8
344        MASK_INFER_UPWARD = 16
345        MASK_INFER_DOWNWARD = 32
346
347        error = False
348
349        self.tempDb.commit()
350
351        # 1. Calculate for orphaned file storage items
352#         self.tempDb.execSql("update fStorItems set calcaction=action, errormsg=''")
353
354        # Reset
355        # Rewriting ACTION_COLLECT (=3) to CALCACTION_COLLECT (=5), copying
356        # otherwise
357        self.tempDb.execSql("""
358                update fStorItems set calcaction = case
359                    when action == 3 then 5
360                    else action
361                end,
362                errormsg=''""")
363
364
365        if orphanedActionDefault == 3:
366#             orphanedActionDefault = CALCACTION_COLLECT
367#             collectorBit = MASK_COLLECT
368
369            # Set collector bit for defaults if default action is "Collect"
370            self.tempDb.execSql("update fStorItems set calcaction=4 "
371                    "where calcaction==0")
372
373#         if orphanedActionDefault == CALCACTION_COLLECT:
374
375
376        # First process explicit (non-default) settings
377
378        # If something should be kept (with/without putting on collector)
379        # all ancestor containers must be kept
380        self.tempDb.execSqlUntilNoChange("update fStorItems set calcaction=calcaction|1|16 "
381                "where (calcaction & 1) == 0 and id in (select containerId from fStorItems "
382                "where (calcaction & 1) == 1)")
383
384        if orphanedDownwardRef:
385            # If a directory is kept explicitly and not by the upward inference
386            # above, everything in it with default setting is also kept
387            self.tempDb.execSqlUntilNoChange("update fStorItems "
388                    "set calcaction=calcaction|1|32 "
389                    "where (calcaction & 3) == 0 and containerId in "
390                    "(select id from fStorItems where (calcaction & 1) == 1 and "
391                    "(calcaction & 16) == 0)")
392
393        # If something (a directory) should be deleted, everything in it
394        # must be deleted as well
395        self.tempDb.execSqlUntilNoChange("update fStorItems "
396                "set calcaction=calcaction|2|32 "
397                "where (calcaction & 2) == 0 and containerId in "
398                "(select id from fStorItems where (calcaction & 2) == 2)")
399
400        # Enforce rule: keep wins over delete
401        self.tempDb.execSql("update fStorItems set calcaction=calcaction & ~2 "
402                "where (calcaction & 3) == 3")
403
404#         # Fill in default for the rest
405#         self.tempDb.execSql("update fStorItems set calcaction=? where "
406#                 "calcaction==0", (orphanedActionDefault | MASK_DEFAULT,))
407
408        # Final cleanup
409        self.tempDb.execSql("""
410                update fStorItems set calcaction = case
411                    when (calcaction & 2) == 2 then 2  /* Delete */
412                    when (calcaction & 5) == 5 then 3  /* Collect */
413                    when (calcaction & 1) == 1 then 1  /* Keep */
414                    when (calcaction & 3) == 0 then ?  /* Default */
415                    else 0 /* Internal error */
416                end,
417                errormsg=''""", (orphanedActionDefault,))
418
419
420        self.tempDb.commit()
421
422
423    def _deleteOrphanedItems(self):
424        """
425        Delete orphaned items which where selected for deletion. Items are
426        deleted in the order of their deepness, meaning items with most path
427        elements first. Not the most efficient but simplest and safest way.
428        """
429        for p in self.tempDb.execSqlQuerySingleColumn("select fullpath "
430                "from fStorItems where calcaction == 2 order by deepness desc"):
431            try:
432                deleteFile(p)
433            except:
434                traceback.print_exc()
435
436
437    def _pruneEmptyDirsFromFileStorage(self):
438        pass # TODO
439#         if not os.path.isdir(self.fileStorDir):
440#             # No file storage present
441#             return
442
443    def _listOrphanedFilesOnCollector(self, collectorPageName):
444        """
445        Add links to collectorPageName of files which are set to be placed there
446        """
447        absPaths = self.tempDb.execSqlQuerySingleColumn("select fullpath "
448                "from fStorItems where calcaction == 3 order by procCounter")
449
450        if len(absPaths) == 0:
451            # Nothing to do
452            return
453
454        config = self.mainControl.getConfig()
455
456        try:
457            prefix = StringOps.strftimeUB(StringOps.unescapeForIni(config.get(
458                "main", "editor_filePaste_prefix", u"")))
459        except:
460            traceback.print_exc()
461            prefix = u""   # TODO Error message?
462
463        try:
464            middle = StringOps.strftimeUB(StringOps.unescapeForIni(config.get(
465                "main", "editor_filePaste_middle", u" ")))
466        except:
467            traceback.print_exc()
468            middle = u" "   # TODO Error message?
469
470        try:
471            suffix = StringOps.strftimeUB(StringOps.unescapeForIni(config.get(
472                "main", "editor_filePaste_suffix", u"")))
473        except:
474            traceback.print_exc()
475            suffix = u""   # TODO Error message?
476
477        bracketedUrl = config.getboolean("main",
478                "editor_filePaste_bracketedUrl", True)
479
480        langHelper = wx.GetApp().createWikiLanguageHelper(
481                self.wikiDocument.getWikiDefaultWikiLanguage())
482
483        urls = [langHelper.createUrlLinkFromPath(self.wikiDocument, ap,
484                relative=True, bracketed=bracketedUrl) for ap in absPaths]
485
486        page = self.wikiDocument.getWikiPageNoError(collectorPageName)
487        page.appendLiveText(prefix + middle.join(urls) + suffix)
488
489
490    def runAfterDialog(self, collectorPageName):
491        """
492        Apply actions after dialog was closed with "OK". IMPORTANT:
493        calcActionAndErrorsDuringDialog() must have been run for the final
494        db state before this function can be called.
495        """
496        self._deleteOrphanedItems()
497#         self._pruneEmptyDirsFromFileStorage()  # as an option
498        self._listOrphanedFilesOnCollector(collectorPageName)
499
500
501
502
503
504class _OrphanedGrid(EnhancedGrid):
505    # Because these contain localized strings, creation must be delayed
506    ACTIONCHOICELIST = None
507    CALCACTIONNAMES = None
508
509    COLOR_GRAY = wx.Colour(200, 200, 200)
510
511    ACTION_DEFAULT = 0
512    ACTION_KEEP = 1
513    ACTION_DELETE = 2
514    ACTION_LINK_ON_COLLECTOR = 3
515
516    COL_RELPATH = 0
517    COL_TYPE = 1
518    COL_ACTION = 2
519    COL_CALCACTION = 3   # Calculated action (inferred from other actions and dependencies)
520
521    COL_COUNT = 4
522
523
524    def __init__(self, parent, db, wikiDocument, collator, id=-1):
525        EnhancedGrid.__init__(self, parent, id)
526
527        if _OrphanedGrid.ACTIONCHOICELIST is None:
528            _OrphanedGrid.ACTIONCHOICELIST = [_(u"Default"), _(u"Keep"),
529                    _(u"Delete"), _(u"Collect")]
530            _OrphanedGrid.CALCACTIONNAMES = [_(u""), _(u"Keep"),
531                    _(u"Delete"), _(u"Collect")]
532
533        self.fileCleanupDialog = parent
534        self.db = db
535        self.wikiDocument = wikiDocument
536        self.collator = collator
537
538        self.gridToId = []
539
540#         dbData = self.db.getSqlDb().execSqlQuery(
541#                 "select relpath, type, id, containerId "
542#                 "from fStorItems")
543#
544#         self.collator.sortByFirst(dbData)
545#
546#         actDef = _(u"Default")
547#         typStr = [_(u"File"), _(u"Dir.")]
548#
549#         self.gridData = [(relPath, typStr[typ], id, containerId)
550#                 for relPath, typ, id, containerId in dbData]
551
552        self.CreateGrid(0, self.COL_COUNT)
553
554        self.SetColLabelValue(self.COL_RELPATH, _(u"Path"))
555        self.SetColLabelValue(self.COL_TYPE, _(u"Type"))
556        self.SetColLabelValue(self.COL_ACTION, _(u"Action"))
557        self.SetColLabelValue(self.COL_CALCACTION, _(u"Calc. action"))
558#         self.SetColLabelValue(self.COL_ERROR, _(u"Error"))
559
560        colWidthSum = sum(self.GetColSize(i) for i in range(self.COL_COUNT))
561        self.SetMinSize((min(colWidthSum + 40, 600), -1))
562#         self.GetParent().SetMinSize((min(colWidthSum + 20, 600), -1))
563
564        readOnlyAttr = wx.grid.GridCellAttr()
565        readOnlyAttr.SetReadOnly()
566        readOnlyAttr.SetBackgroundColour(self.COLOR_GRAY)
567
568        self.SetColAttr(self.COL_RELPATH, readOnlyAttr)
569        self.SetColAttr(self.COL_TYPE, readOnlyAttr)
570        self.SetColAttr(self.COL_CALCACTION, readOnlyAttr)
571#         self.SetColAttr(self.COL_ERROR, readOnlyAttr)
572
573        actionChEditor = wx.grid.GridCellChoiceEditor(self.ACTIONCHOICELIST,
574                False)
575        actionAttr = wx.grid.GridCellAttr()
576        actionAttr.SetEditor(actionChEditor)
577
578        self.SetColAttr(self.COL_ACTION, actionAttr)
579
580        self.updateGridBySql()
581
582
583#     def updateErrorColumn(self):
584#         """
585#         Update error column of table according to the messages in the
586#         grid data entries
587#         """
588#
589#         for rowNo, row in enumerate(self.gridData):
590#             if obj.errorMessage:
591#                 self.SetCellBackgroundColour(rowNo, self.COL_RELPATH, wx.RED)
592#                 self.SetCellValue(rowNo, self.COL_ERROR, obj.errorMessage)
593#             else:
594#                 self.SetCellBackgroundColour(rowNo, self.COL_RELPATH,
595#                         self.COLOR_GRAY)
596#                 self.SetCellValue(rowNo, self.COL_ERROR, "")
597
598
599    def _isDirectEdit(self, row, col):
600        return True
601
602
603    def OnGridSelectCell(self, evt):
604        evt.Skip()
605#         self.fileCleanupDialog.setGridErrorMessage(
606#                 self.gridData[evt.GetRow()][1].errorMessage)
607
608
609    def updateGridBySql(self, orphanedActionDefault=0):
610        dbData = self.db.getSqlDb().execSqlQuery(
611                "select relpath, type, id, action, calcaction, errormsg "
612                "from fStorItems")
613
614        self.collator.sortByFirst(dbData)
615
616        actDef = _(u"Default")
617        typStr = [_(u"File"), _(u"Directory")]
618
619        if self.GetNumberRows() > 0:
620            self.DeleteRows(0, self.GetNumberRows())
621        self.AppendRows(len(dbData))
622
623        self.gridToId = []
624
625        for rowNo, (relPath, typ, id, action, calcaction, errormsg) \
626                in enumerate(dbData):
627            self.gridToId.append(id)
628
629            self.SetCellValue(rowNo, self.COL_RELPATH, relPath)
630            self.SetCellValue(rowNo, self.COL_TYPE, typStr[typ])
631            self.SetCellValue(rowNo, self.COL_ACTION,
632                    self.ACTIONCHOICELIST[action])
633            self.SetCellValue(rowNo, self.COL_CALCACTION,
634                    self.CALCACTIONNAMES[calcaction])
635
636            if calcaction != 0 and orphanedActionDefault != 0:
637                if action == 0:
638                    action = orphanedActionDefault
639                if action != calcaction:
640                    self.SetCellBackgroundColour(rowNo, self.COL_CALCACTION, wx.RED)
641                else:
642                    self.SetCellBackgroundColour(rowNo, self.COL_CALCACTION,
643                            self.COLOR_GRAY)
644            else:
645                self.SetCellBackgroundColour(rowNo, self.COL_CALCACTION,
646                        self.COLOR_GRAY)
647
648
649
650    def storeGridToSql(self):
651        sqlDb = self.db.getSqlDb()
652
653        for rowNo, id in enumerate(self.gridToId):
654#             print "--storeGridToSql5", repr((rowNo, id, self.GetCellValue(rowNo,
655#                     self.COL_ACTION), self.ACTIONCHOICELIST.index(self.GetCellValue(rowNo,
656#                     self.COL_ACTION))))
657            sqlDb.execSql("update fStorItems set action=? where id==?",
658                    (self.ACTIONCHOICELIST.index(self.GetCellValue(rowNo,
659                    self.COL_ACTION)), id))
660
661
662
663
664
665
666class _MissingGrid(EnhancedGrid):
667    # Because these contain localized strings, creation must be delayed
668    ACTIONCHOICELIST = None
669
670    COLOR_GRAY = wx.Colour(200, 200, 200)
671
672    ACTION_DEFAULT = 0
673    ACTION_KEEP = 1
674    ACTION_DELETE = 2
675
676    COL_FULLPATH = 0
677    COL_ACTION = 1
678
679    COL_COUNT = 2
680
681
682    def __init__(self, parent, db, wikiDocument, collator, id=-1):
683        EnhancedGrid.__init__(self, parent, id)
684
685        if _MissingGrid.ACTIONCHOICELIST is None:
686            _MissingGrid.ACTIONCHOICELIST = [_(u"Default"), _(u"Keep"),
687                    _(u"Delete")]
688
689        self.fileCleanupDialog = parent
690        self.db = db
691        self.wikiDocument = wikiDocument
692        self.collator = collator
693
694        self.gridToId = []
695
696#         dbData = self.db.getSqlDb().execSqlQuery(
697#                 "select relpath, type, id, containerId "
698#                 "from fStorItems")
699#
700#         self.collator.sortByFirst(dbData)
701#
702#         actDef = _(u"Default")
703#         typStr = [_(u"File"), _(u"Dir.")]
704#
705#         self.gridData = [(relPath, typStr[typ], id, containerId)
706#                 for relPath, typ, id, containerId in dbData]
707
708        self.CreateGrid(0, self.COL_COUNT)
709
710        self.SetColLabelValue(self.COL_FULLPATH, _(u"Path"))
711        self.SetColLabelValue(self.COL_ACTION, _(u"Action"))
712#         self.SetColLabelValue(self.COL_ERROR, _(u"Error"))
713
714        colWidthSum = sum(self.GetColSize(i) for i in range(self.COL_COUNT))
715        self.SetMinSize((min(colWidthSum + 40, 600), -1))
716#         self.GetParent().SetMinSize((min(colWidthSum + 20, 600), -1))
717
718        readOnlyAttr = wx.grid.GridCellAttr()
719        readOnlyAttr.SetReadOnly()
720        readOnlyAttr.SetBackgroundColour(self.COLOR_GRAY)
721
722        self.SetColAttr(self.COL_FULLPATH, readOnlyAttr)
723
724        actionChEditor = wx.grid.GridCellChoiceEditor(self.ACTIONCHOICELIST,
725                False)
726        actionAttr = wx.grid.GridCellAttr()
727        actionAttr.SetEditor(actionChEditor)
728
729        self.SetColAttr(self.COL_ACTION, actionAttr)
730
731        self.updateGridBySql()
732
733        self.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.OnSelectCell)
734        self.Bind(wx.grid.EVT_GRID_CMD_CELL_LEFT_DCLICK, self.OnCellDClick)
735
736#     def updateErrorColumn(self):
737#         """
738#         Update error column of table according to the messages in the
739#         grid data entries
740#         """
741#
742#         for rowNo, row in enumerate(self.gridData):
743#             if obj.errorMessage:
744#                 self.SetCellBackgroundColour(rowNo, self.COL_RELPATH, wx.RED)
745#                 self.SetCellValue(rowNo, self.COL_ERROR, obj.errorMessage)
746#             else:
747#                 self.SetCellBackgroundColour(rowNo, self.COL_RELPATH,
748#                         self.COLOR_GRAY)
749#                 self.SetCellValue(rowNo, self.COL_ERROR, "")
750
751
752    def _isDirectEdit(self, row, col):
753        return True
754
755
756    def OnSelectCell(self, evt):
757        self.fileCleanupDialog.updateMissingLinkingPagesListBoxByRefedItemId(
758                self.gridToId[evt.GetRow()])
759        evt.Skip()
760#         self.fileCleanupDialog.setGridErrorMessage(
761#                 self.gridData[evt.GetRow()][1].errorMessage)
762
763
764    def OnCellDClick(self, evt):
765        self.fileCleanupDialog.runFirstInMissingLinkingPagesListBox()
766
767
768    def updateGridBySql(self):
769        dbData = self.db.getSqlDb().execSqlQuery(
770                "select fullpath, id, present, action "
771                "from refedItems")
772
773        self.collator.sortByFirst(dbData)
774
775        actDef = _(u"Default")
776       
777        row = self.GetGridCursorRow()
778        col = self.GetGridCursorCol()
779
780        if self.GetNumberRows() > 0:
781            self.DeleteRows(0, self.GetNumberRows())
782        self.AppendRows(len(dbData))
783
784        self.gridToId = []
785
786        for rowNo, (fullPath, id, present, action) \
787                in enumerate(dbData):
788            self.gridToId.append(id)
789
790            self.SetCellValue(rowNo, self.COL_FULLPATH, fullPath)
791            self.SetCellValue(rowNo, self.COL_ACTION,
792                    self.ACTIONCHOICELIST[action])
793
794        self.SetGridCursor(row, col)
795
796
797    def storeGridToSql(self):
798        sqlDb = self.db.getSqlDb()
799
800        for rowNo, id in enumerate(self.gridToId):
801#             print "--storeGridToSql5", repr((rowNo, id, self.GetCellValue(rowNo,
802#                     self.COL_ACTION), self.ACTIONCHOICELIST.index(self.GetCellValue(rowNo,
803#                     self.COL_ACTION))))
804            sqlDb.execSql("update refedItems set action=? where id==?",
805                    (self.ACTIONCHOICELIST.index(self.GetCellValue(rowNo,
806                    self.COL_ACTION)), id))
807
808
809
810
811class _MissingLinkingPagesItemInfo(object):
812    __slots__ = ("__weakref__", "unifName", "wikiWord", "hitList",
813            "fileCleanupDialog")
814
815    def __init__(self, fileCleanupDialog, unifName, hitList):
816        self.fileCleanupDialog = fileCleanupDialog
817       
818        self.unifName = unifName
819        if unifName.startswith(u"wikipage/"):
820            self.wikiWord = unifName[9:]
821        else:
822            self.wikiWord = unifName  # TODO: This should never happen
823           
824        hitList.sort()
825        self.hitList = hitList
826
827
828#     def buildOccurrence(self, text, before, after, pos, occNumber, maxOccCount):
829#         self.html = None
830#         basum = before + after
831#         self.occNumber = -1
832#         self.occPos = pos
833#         self.maxCountOccurrences = maxOccCount
834#
835#         if basum == 0:
836#             # No context
837#             self.occHtml = u""
838#             return self
839#         
840#         if pos[0] is None:
841#             # All occurences where deleted meanwhile dialog was open
842#             self.occHtml = u""
843#             self.occNumber = 0
844#             self.occCount = 0
845#             return self
846#         
847#         if pos[0] == -1:
848#             # No position -> use beginning of text
849#             self.occHtml = escapeHtml(text[0:basum])
850#             return self
851#         
852#         s = max(0, pos[0] - before)
853#         e = min(len(text), pos[1] + after)
854#         self.occHtml = u"".join([escapeHtml(text[s:pos[0]]),
855#             "<b>", escapeHtml(text[pos[0]:pos[1]]), "</b>",
856#             escapeHtml(text[pos[1]:e])])
857#             
858#         self.occNumber = occNumber
859#         return self
860
861
862    def getCharSelection(self):
863        if len(self.hitList) == 0:
864            return (-1, -1)
865       
866        return (self.hitList[0][0], self.hitList[0][1])
867
868
869    def getHtml(self):
870        modified = self.fileCleanupDialog.isModifiedPage(self.unifName)
871       
872        if modified:
873            result = [u'<font color="GRAY"><b>%s</b>' % \
874                    StringOps.escapeHtml(self.wikiWord)]
875        else:
876            result = [u'<font color="BLUE"><b>%s</b></font>' % \
877                    StringOps.escapeHtml(self.wikiWord)]
878       
879        if len(self.hitList) > 4:
880            for hit in self.hitList[:3]:
881                result.append(u"<br />\n%s" % StringOps.escapeHtml(hit[2]))
882
883            result.append(u"<br />\n...")
884        else:
885            for hit in self.hitList:
886                result.append(u"<br />\n%s" % StringOps.escapeHtml(hit[2]))
887
888        if modified:
889            result.append(u"</font>")
890
891        return u"".join(result)
892
893
894
895
896class _MissingLinkingPagesListBox(wx.HtmlListBox):
897    def __init__(self, parent, db, mainControl, collator, ID):
898        wx.HtmlListBox.__init__(self, parent, ID, style = wx.SUNKEN_BORDER)
899
900        self.fileCleanupDialog = parent
901        self.db = db
902        self.mainControl = mainControl
903        self.collator = collator
904        self.lastRefedItemId = -1
905
906        self.itemInfo = []
907        self.SetItemCount(0)
908       
909        wx.EVT_KILL_FOCUS(self, self.OnKillFocus)
910        wx.EVT_LISTBOX_DCLICK(self, ID, self.OnDClick)
911
912#         self.contextMenuSelection = -2
913
914#         wx.EVT_LEFT_DOWN(self, self.OnLeftDown)
915#         wx.EVT_LEFT_DCLICK(self, self.OnLeftDown)
916#         wx.EVT_MIDDLE_DOWN(self, self.OnMiddleButtonDown)
917#         wx.EVT_KEY_DOWN(self, self.OnKeyDown)
918#         wx.EVT_CONTEXT_MENU(self, self.OnContextMenu)
919#
920#
921#         wx.EVT_MENU(self, GUI_ID.CMD_ACTIVATE_THIS, self.OnActivateThis)
922#         wx.EVT_MENU(self, GUI_ID.CMD_ACTIVATE_NEW_TAB_THIS,
923#                 self.OnActivateNewTabThis)
924#         wx.EVT_MENU(self, GUI_ID.CMD_ACTIVATE_NEW_TAB_BACKGROUND_THIS,
925#                 self.OnActivateNewTabBackgroundThis)
926
927
928    def OnKillFocus(self, evt):
929        self.SetSelection(-1)
930
931
932    def OnGetItem(self, i):
933#         if self.isShowingSearching:
934#             return u"<b>" + _(u"Searching... (click into field to abort)") + u"</b>"
935#         elif self.GetCount() == 0:
936#             return u"<b>" + _(u"Not found") + u"</b>"
937
938        try:
939            return self.itemInfo[i].getHtml()
940        except IndexError:
941            return u""
942
943
944    def updateByRefedItemId(self, refedItemId):
945        self.lastRefedItemId = refedItemId
946        dbData = self.db.getSqlDb().execSqlQuery("select unifName, "
947                "corePos, coreLength, presentationUrl from unifNameToItem "
948                "where refedItemId == ?", (refedItemId,))
949
950        if len(dbData) == 0:
951            self.SetItemCount(0)
952            return
953
954        # Group by unified name (wiki word)
955        groupDict = {}
956        for un, cp, cl, pu in dbData:
957            groupDict.setdefault(un, []).append((cp, cl, pu))
958
959        finalData = groupDict.items()
960
961        self.collator.sortByFirst(finalData)
962
963        self.itemInfo = [_MissingLinkingPagesItemInfo(self.fileCleanupDialog,
964                un, hitList) for un, hitList in finalData]
965
966        self.SetItemCount(len(self.itemInfo))
967        self.Refresh()
968
969
970    def update(self):
971        if self.lastRefedItemId > -1:
972            self.updateByRefedItemId(self.lastRefedItemId)
973
974
975    def activateSelection(self, sel):
976        if sel == -1 or self.GetItemCount() == 0:
977            return
978
979        info = self.itemInfo[sel]
980
981        self.mainControl.openWikiPage(info.wikiWord)
982
983        editor = self.mainControl.getActiveEditor()
984        if editor is not None:
985            csel = info.getCharSelection()
986            if csel[0] != -1:
987                self.mainControl.getActiveEditor().showSelectionByCharPos(
988                        csel[0], csel[0] + csel[1])
989#                 self.mainControl.getActiveEditor().ensureSelectionExpanded()
990
991            # Works in fast search popup only if called twice
992            # (keeping it here just in case)
993            editor.SetFocus()
994            editor.SetFocus()
995
996
997    def OnDClick(self, evt):
998        self.activateSelection(self.GetSelection())
999
1000
1001
1002#
1003#
1004#     def OnLeftDown(self, evt):
1005#         if self.isShowingSearching:
1006#             self.searchWikiDialog.stopSearching()
1007#
1008#         if self.GetItemCount() == 0:
1009#             return  # no evt.Skip()?
1010#
1011#         pos = evt.GetPosition()
1012#         hitsel = self.HitTest(pos)
1013#         
1014#         if hitsel == wx.NOT_FOUND:
1015#             evt.Skip()
1016#             return
1017#         
1018#         if pos.x < (5 + 6):
1019#             # Click inside the blue bar
1020#             self.SetSelection(hitsel)
1021#             self._pageListFindNext()
1022#             return
1023#         
1024#         evt.Skip()
1025#
1026#
1027#     def OnMiddleButtonDown(self, evt):
1028#         if self.GetItemCount() == 0:
1029#             return  # no evt.Skip()?
1030#
1031#         pos = evt.GetPosition()
1032#         if pos == wx.DefaultPosition:
1033#             hitsel = self.GetSelection()
1034#
1035#         hitsel = self.HitTest(pos)
1036#
1037#         if hitsel == wx.NOT_FOUND:
1038#             evt.Skip()
1039#             return
1040#
1041#         if pos.x < (5 + 6):
1042#             # Click inside the blue bar
1043#             self.SetSelection(hitsel)
1044#             self._pageListFindNext()
1045#             return
1046#         
1047#         info = self.itemInfo[hitsel]
1048#
1049#         if evt.ControlDown():
1050#             configCode = self.mainControl.getConfig().getint("main",
1051#                     "mouse_middleButton_withCtrl")
1052#         else:
1053#             configCode = self.mainControl.getConfig().getint("main",
1054#                     "mouse_middleButton_withoutCtrl")
1055#                     
1056#         tabMode = MIDDLE_MOUSE_CONFIG_TO_TABMODE[configCode]
1057#
1058#         presenter = self.mainControl.activatePageByUnifiedName(
1059#                 u"wikipage/" + info.wikiWord, tabMode)
1060#         
1061#         if presenter is None:
1062#             return
1063#
1064#         if info.occPos[0] != -1:
1065#             presenter.getSubControl("textedit").showSelectionByCharPos(
1066#                     info.occPos[0], info.occPos[1])
1067#
1068#         if configCode != 1:
1069#             # If not new tab opened in background -> focus editor
1070#
1071#             # Works in fast search popup only if called twice
1072#             self.mainControl.getActiveEditor().SetFocus()
1073#             self.mainControl.getActiveEditor().SetFocus()
1074#
1075#         
1076#     def OnKeyDown(self, evt):
1077#         if self.GetItemCount() == 0:
1078#             return  # no evt.Skip()?
1079#
1080#         accP = getAccelPairFromKeyDown(evt)
1081#         matchesAccelPair = self.mainControl.keyBindings.matchesAccelPair
1082#         
1083#         if matchesAccelPair("ContinueSearch", accP):
1084#             # ContinueSearch is normally F3
1085#             self._pageListFindNext()
1086#         elif accP == (wx.ACCEL_NORMAL, wx.WXK_RETURN) or \
1087#                 accP == (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_ENTER):
1088#             self.OnDClick(evt)
1089#         else:
1090#             evt.Skip()
1091#
1092#
1093#     def OnContextMenu(self, evt):
1094#         if self.GetItemCount() == 0:
1095#             return  # no evt.Skip()?
1096#
1097#         pos = evt.GetPosition()
1098#         if pos == wx.DefaultPosition:
1099#             hitsel = self.GetSelection()
1100#         else:
1101#             hitsel = self.HitTest(self.ScreenToClient(pos))
1102#
1103#         if hitsel == wx.NOT_FOUND:
1104#             evt.Skip()
1105#             return
1106#
1107#         self.contextMenuSelection = hitsel
1108#         try:
1109#             menu = wx.Menu()
1110#             appendToMenuByMenuDesc(menu, _CONTEXT_MENU_ACTIVATE)
1111#             self.PopupMenu(menu)
1112#             menu.Destroy()
1113#         finally:
1114#             self.contextMenuSelection = -2
1115#
1116#
1117#
1118#     def OnActivateThis(self, evt):
1119#         if self.contextMenuSelection > -1:
1120#             info = self.itemInfo[self.contextMenuSelection]
1121#
1122# #             presenter = self.mainControl.activateWikiWord(info.wikiWord, 0)
1123#             presenter = self.mainControl.activatePageByUnifiedName(
1124#                     u"wikipage/" + info.wikiWord, 0)
1125#
1126#             if presenter is None:
1127#                 return
1128#
1129#             if info.occPos[0] != -1:
1130#                 presenter.getSubControl("textedit").showSelectionByCharPos(
1131#                         info.occPos[0], info.occPos[1])
1132#     
1133#             # Works in fast search popup only if called twice
1134#             self.mainControl.getActiveEditor().SetFocus()
1135#             self.mainControl.getActiveEditor().SetFocus()
1136#
1137#
1138#     def OnActivateNewTabThis(self, evt):
1139#         if self.contextMenuSelection > -1:
1140#             info = self.itemInfo[self.contextMenuSelection]
1141#
1142# #             presenter = self.mainControl.activateWikiWord(info.wikiWord, 2)
1143#             presenter = self.mainControl.activatePageByUnifiedName(
1144#                     u"wikipage/" + info.wikiWord, 2)
1145#
1146#             if presenter is None:
1147#                 return
1148#
1149#             if info.occPos[0] != -1:
1150#                 presenter.getSubControl("textedit").showSelectionByCharPos(
1151#                         info.occPos[0], info.occPos[1])
1152#     
1153#             # Works in fast search popup only if called twice
1154#             self.mainControl.getActiveEditor().SetFocus()
1155#             self.mainControl.getActiveEditor().SetFocus()
1156#
1157#
1158#     def OnActivateNewTabBackgroundThis(self, evt):
1159#         if self.contextMenuSelection > -1:
1160#             info = self.itemInfo[self.contextMenuSelection]
1161#
1162# #             presenter = self.mainControl.activateWikiWord(info.wikiWord, 3)
1163#             presenter = self.mainControl.activatePageByUnifiedName(
1164#                     u"wikipage/" + info.wikiWord, 3)
1165#             
1166#             if presenter is None:
1167#                 return
1168#
1169#             if info.occPos[0] != -1:
1170#                 presenter.getSubControl("textedit").showSelectionByCharPos(
1171#                         info.occPos[0], info.occPos[1])
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185#
1186#
1187#
1188# class MissingGridRow(object):
1189#     """
1190#     One row in the grid of missing files (referenced but not present)
1191#     """
1192#     __slots__ = ("refedItemsId", "fullPath", "itemName", "action",
1193#             "changeLinkTo")
1194#
1195#     ACTION_DEFAULT = 0
1196#     ACTION_NONE = 1
1197#     ACTION_REMOVE_LINKS = 2
1198#     ACTION_CHANGE_LINKS = 3
1199
1200
1201
1202
1203class FileCleanupInitialDialog(wx.Dialog):
1204    """
1205    Dialog to ask for the options for the data to collect on file links
1206    and file storage
1207    """
1208
1209    def __init__(self, mainControl, parent):
1210        d = wx.PreDialog()
1211        self.PostCreate(d)
1212
1213        self.mainControl = mainControl
1214        self.value = None
1215
1216        res = wx.xrc.XmlResource.Get()
1217        res.LoadOnDialog(self, parent, "FileCleanupInitialDialog")
1218
1219        self.ctrls = XrcControls(self)
1220
1221        value = {"downwardRef": True}
1222        self.ctrls.cbDownwardRef.SetValue(value["downwardRef"])
1223
1224        self.ctrls.btnOk.SetId(wx.ID_OK)
1225        self.ctrls.btnCancel.SetId(wx.ID_CANCEL)
1226
1227        self.Fit()
1228
1229        # Fixes focus bug under Linux
1230        self.SetFocus()
1231
1232        wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk)
1233
1234
1235    def GetValue(self):
1236        return self.value
1237
1238
1239    def OnOk(self, evt):
1240        self.value = {"downwardRef": self.ctrls.cbDownwardRef.GetValue()}
1241
1242        self.EndModal(wx.ID_OK)
1243
1244FileCleanupInitialDialog.runModal = staticmethod(runDialogModalFactory(
1245        FileCleanupInitialDialog))
1246
1247
1248
1249
1250class FileCleanupDialog(wx.Dialog):
1251    """
1252    """
1253
1254    # Because these contain localized strings, creation must be delayed
1255    ORPHANED_DEFAULTACTIONCHOICELIST = None
1256
1257    def __init__(self, mainControl, db, parent):
1258
1259        if FileCleanupDialog.ORPHANED_DEFAULTACTIONCHOICELIST is None:
1260            FileCleanupDialog.ORPHANED_DEFAULTACTIONCHOICELIST = [_(u"Keep"),
1261                    _(u"Delete"), _(u"Collect")]
1262
1263        d = wx.PreDialog()
1264        self.PostCreate(d)
1265
1266        self.mainControl = mainControl
1267        self.db = db
1268        self.value = False
1269
1270        self.modifiedSinceOpen = set() # Set of unified names of pages modified
1271            # since dialog was opened
1272
1273        res = wx.xrc.XmlResource.Get()
1274        res.LoadOnDialog(self, parent, "FileCleanupDialog")
1275
1276        self.ctrls = XrcControls(self)
1277
1278        orphanedGrid = _OrphanedGrid(self, self.db, mainControl.getWikiDocument(),
1279                mainControl.getCollator())
1280
1281        res.AttachUnknownControl("gridOrphaned", orphanedGrid, self)
1282       
1283        missingGrid = _MissingGrid(self, self.db, mainControl.getWikiDocument(),
1284                mainControl.getCollator())
1285
1286        res.AttachUnknownControl("gridMissing", missingGrid, self)
1287
1288        htmllbMissingLinkingPages = _MissingLinkingPagesListBox(self, self.db,
1289                mainControl, mainControl.getCollator(),
1290                GUI_ID.htmllbMissingLinkingPages)
1291
1292        res.AttachUnknownControl("htmllbMissingLinkingPages",
1293                htmllbMissingLinkingPages, self)
1294
1295        self.ctrls.btnOk.SetId(wx.ID_OK)
1296        self.ctrls.btnCancel.SetId(wx.ID_CANCEL)
1297
1298        self.Fit()
1299        # If the table is too long, a resizing may become necessary
1300        WindowLayout.setWindowSize(self)
1301        WindowLayout.setWindowPos(self)
1302
1303        # Fixes layout problem
1304        orphanedGrid.GetGrandParent().Layout()
1305        missingGrid.GetGrandParent().Layout()
1306
1307        # Fixes focus bug under Linux
1308        self.SetFocus()
1309
1310        wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk)
1311        wx.EVT_BUTTON(self, GUI_ID.btnTest, self.OnTest)
1312       
1313
1314        self.__sinkWikiDoc = wxKeyFunctionSink((
1315                ("renamed wiki page", self.onRemovedWikiPage),
1316                ("deleted wiki page", self.onRemovedWikiPage),
1317                ("updated wiki page", self.onModifiedWikiPage),
1318        ), self.mainControl.getWikiDocument().getMiscEvent(), self)
1319       
1320       
1321       
1322
1323#         wx.EVT_TEXT(self, ID, self.OnText)
1324#         wx.EVT_CHAR(self.ctrls.text, self.OnCharText)
1325#         wx.EVT_CHAR(self.ctrls.lb, self.OnCharListBox)
1326#         wx.EVT_LISTBOX(self, ID, self.OnListBox)
1327#         wx.EVT_LISTBOX_DCLICK(self, GUI_ID.lb, self.OnOk)
1328
1329    def GetValue(self):
1330        return self.value
1331
1332
1333    def OnOk(self, evt):
1334#         if not self.ctrls.gridDetails.validateInput():
1335#             self.ctrls.gridDetails.updateErrorColumn()
1336#             return
1337#
1338#         self.ctrls.gridDetails.writeGridToDb()
1339        self.ctrls.gridOrphaned.storeGridToSql()
1340        self.db.calcActionAndErrorsDuringDialog(
1341                self.ctrls.chOrphanedDefaultAction.GetSelection() + 1,
1342                self.ctrls.cbOrphanedDownwardRef.GetValue())
1343
1344        self.db.getSqlDb().commit()
1345
1346        absPaths = self.db.getSqlDb().execSqlQuerySingleColumn("select fullpath "
1347                "from fStorItems where calcaction == 3 limit 1")
1348
1349        collectorPageName = self.ctrls.tfOrphanedCollectorPage.GetValue()
1350
1351        if len(absPaths) > 0:
1352            # Some files are designated to be collected on collector page
1353            langHelper = wx.GetApp().createWikiLanguageHelper(
1354                    self.mainControl.getWikiDocument()\
1355                    .getWikiDefaultWikiLanguage())
1356
1357            if langHelper.checkForInvalidWikiWord(collectorPageName,
1358                    self.mainControl.getWikiDocument()) is not None:
1359                # There are paths to collect but the name of the collector
1360                # page isn't a valid wiki word, so update UI and back to user
1361                self.OnTest(None)
1362                return
1363
1364        self.value = True
1365
1366        self.db.runAfterDialog(collectorPageName)
1367
1368        if self is self.mainControl.nonModalFileCleanupDlg:
1369            self.mainControl.nonModalFileCleanupDlg = None
1370
1371        self.__sinkWikiDoc.disconnect()
1372
1373        if self.IsModal():
1374            self.EndModal(wx.ID_OK)
1375        else:
1376            self.Destroy()
1377
1378
1379
1380    def OnClose(self, evt):
1381        self.value = None
1382
1383        if self is self.mainControl.nonModalFileCleanupDlg:
1384            self.mainControl.nonModalFileCleanupDlg = None
1385
1386        self.__sinkWikiDoc.disconnect()
1387
1388        if self.IsModal():
1389            self.EndModal(wx.ID_CANCEL)
1390        else:
1391            self.Destroy()
1392
1393
1394    def OnTest(self, evt):
1395        self.ctrls.gridOrphaned.storeGridToSql()
1396        self.db.calcActionAndErrorsDuringDialog(
1397                self.ctrls.chOrphanedDefaultAction.GetSelection() + 1,
1398                self.ctrls.cbOrphanedDownwardRef.GetValue())
1399
1400        self.db.getSqlDb().commit()
1401
1402        absPaths = self.db.getSqlDb().execSqlQuerySingleColumn("select fullpath "
1403                "from fStorItems where calcaction == 3 limit 1")
1404
1405        self.ctrls.tfOrphanedCollectorPage.SetBackgroundColour(wx.WHITE)
1406
1407        collectorPageName = self.ctrls.tfOrphanedCollectorPage.GetValue()
1408
1409        if len(absPaths) > 0:
1410            # Some files are designated to be collected on collector page
1411            langHelper = wx.GetApp().createWikiLanguageHelper(
1412                    self.mainControl.getWikiDocument()\
1413                    .getWikiDefaultWikiLanguage())
1414
1415            if langHelper.checkForInvalidWikiWord(collectorPageName,
1416                    self.mainControl.getWikiDocument()) is not None:
1417                self.ctrls.tfOrphanedCollectorPage.SetBackgroundColour(wx.RED)
1418
1419        self.ctrls.tfOrphanedCollectorPage.Refresh()
1420
1421        self.ctrls.gridOrphaned.updateGridBySql(
1422                self.ctrls.chOrphanedDefaultAction.GetSelection() + 1)
1423
1424        return
1425
1426
1427    def onModifiedWikiPage(self, miscevt):
1428        self.db.updateWikiPage(miscevt.get("wikiPage"))
1429        self.db.deleteUninteresting()
1430
1431#         if miscevt.get("wikiPage").getUnifiedPageName() in self.modifiedSinceOpen:
1432#             return
1433#
1434#         self.modifiedSinceOpen.add(miscevt.get("wikiPage").getUnifiedPageName())
1435        self.ctrls.gridMissing.updateGridBySql()
1436        self.ctrls.htmllbMissingLinkingPages.update()
1437
1438
1439    def onRemovedWikiPage(self, miscevt):
1440        if miscevt.get("wikiPage").getUnifiedPageName() in self.modifiedSinceOpen:
1441            return
1442
1443        self.modifiedSinceOpen.add(miscevt.get("wikiPage").getUnifiedPageName())
1444        self.ctrls.htmllbMissingLinkingPages.update()
1445
1446
1447    def isModifiedPage(self, unifPageName):
1448        return unifPageName in self.modifiedSinceOpen
1449
1450    def updateMissingLinkingPagesListBoxByRefedItemId(self, refedItemId):
1451        self.ctrls.htmllbMissingLinkingPages.updateByRefedItemId(refedItemId)
1452
1453    def runFirstInMissingLinkingPagesListBox(self):
1454        """
1455        Simulate double click on first item in MissingLinkingPagesListBox
1456        Called after double click on a cell in missing items table
1457        """
1458        self.ctrls.htmllbMissingLinkingPages.activateSelection(0)
1459
1460
1461
1462FileCleanupDialog.runModal = staticmethod(runDialogModalFactory(
1463        FileCleanupDialog))
1464
1465
1466
1467
1468def runFileCleanup(mainControl, parent, progresshandler):
1469    if mainControl.nonModalFileCleanupDlg:
1470        # Dialog already open
1471        mainControl.nonModalFileCleanupDlg.SetFocus()
1472        return
1473
1474    options = FileCleanupInitialDialog.runModal(mainControl, parent)
1475    if options is None:
1476        return
1477
1478    db = InfoDatabase(mainControl)
1479    db.buildDatabaseBeforeDialog(progresshandler, options)
1480
1481    dlg = FileCleanupDialog(mainControl, db, parent)
1482    mainControl.nonModalFileCleanupDlg = dlg
1483
1484#     parent.Enable(False)
1485#     dlg.Enable(True)
1486    dlg.Show()
1487
1488#     FileCleanupDialog.runModal(mainControl, db, parent)
Note: See TracBrowser for help on using the browser.