ghelp.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973
  1. """
  2. @package gui_core.ghelp
  3. @brief Help/about window, menu tree, search module tree
  4. Classes:
  5. - ghelp::AboutWindow
  6. - ghelp::HelpFrame
  7. - ghelp::HelpWindow
  8. - ghelp::HelpPanel
  9. (C) 2008-2019 by the GRASS Development Team
  10. This program is free software under the GNU General Public License
  11. (>=v2). Read the file COPYING that comes with GRASS for details.
  12. @author Martin Landa <landa.martin gmail.com>
  13. """
  14. import os
  15. import codecs
  16. import platform
  17. import re
  18. import textwrap
  19. import sys
  20. import six
  21. import wx
  22. from wx.html import HtmlWindow
  23. try:
  24. from wx.lib.agw.hyperlink import HyperLinkCtrl
  25. except ImportError:
  26. from wx.lib.hyperlink import HyperLinkCtrl
  27. try:
  28. from wx.adv import AboutDialogInfo
  29. from wx.adv import AboutBox
  30. except ImportError:
  31. from wx import AboutDialogInfo
  32. from wx import AboutBox
  33. import grass.script as grass
  34. from grass.exceptions import CalledModuleError
  35. # needed just for testing
  36. if __name__ == "__main__":
  37. from grass.script.setup import set_gui_path
  38. set_gui_path()
  39. from core import globalvar
  40. from core.gcmd import GError, DecodeString
  41. from gui_core.widgets import FormNotebook, ScrolledPanel
  42. from gui_core.wrap import Button, StaticText, TextCtrl
  43. from core.debug import Debug
  44. class AboutWindow(wx.Frame):
  45. """Create custom About Window"""
  46. def __init__(self, parent, size=(770, 460), title=_("About GRASS GIS"), **kwargs):
  47. wx.Frame.__init__(
  48. self, parent=parent, id=wx.ID_ANY, title=title, size=size, **kwargs
  49. )
  50. self.panel = wx.Panel(parent=self, id=wx.ID_ANY)
  51. # icon
  52. self.SetIcon(
  53. wx.Icon(os.path.join(globalvar.ICONDIR, "grass.ico"), wx.BITMAP_TYPE_ICO)
  54. )
  55. # notebook
  56. self.aboutNotebook = FormNotebook(self.panel, style=wx.BK_LEFT)
  57. for title, win in (
  58. (_("Info"), self._pageInfo()),
  59. (_("Copyright"), self._pageCopyright()),
  60. (_("License"), self._pageLicense()),
  61. (_("Citation"), self._pageCitation()),
  62. (_("Authors"), self._pageCredit()),
  63. (_("Contributors"), self._pageContributors()),
  64. (_("Extra contributors"), self._pageContributors(extra=True)),
  65. (_("Translators"), self._pageTranslators()),
  66. (_("Translation status"), self._pageStats()),
  67. ):
  68. self.aboutNotebook.AddPage(page=win, text=title)
  69. wx.CallAfter(self.aboutNotebook.SetSelection, 0)
  70. wx.CallAfter(self.aboutNotebook.Refresh)
  71. # buttons
  72. self.btnClose = Button(parent=self.panel, id=wx.ID_CLOSE)
  73. self.btnClose.Bind(wx.EVT_BUTTON, self.OnCloseWindow)
  74. self._doLayout()
  75. def _doLayout(self):
  76. btnSizer = wx.BoxSizer(wx.HORIZONTAL)
  77. btnSizer.Add(self.btnClose, proportion=0, flag=wx.ALL, border=5)
  78. sizer = wx.BoxSizer(wx.VERTICAL)
  79. sizer.Add(self.aboutNotebook, proportion=1, flag=wx.EXPAND | wx.ALL, border=1)
  80. sizer.Add(btnSizer, proportion=0, flag=wx.ALL | wx.ALIGN_RIGHT, border=1)
  81. self.SetMinSize((400, 400))
  82. self.panel.SetSizer(sizer)
  83. sizer.Fit(self.panel)
  84. self.Layout()
  85. def _pageInfo(self):
  86. """Info page"""
  87. # get version and web site
  88. vInfo = grass.version()
  89. if not vInfo:
  90. sys.stderr.write(_("Unable to get GRASS version\n"))
  91. infoTxt = ScrolledPanel(self.aboutNotebook)
  92. infoTxt.SetupScrolling()
  93. infoSizer = wx.BoxSizer(wx.VERTICAL)
  94. infoGridSizer = wx.GridBagSizer(vgap=5, hgap=5)
  95. logo = os.path.join(globalvar.ICONDIR, "grass-64x64.png")
  96. logoBitmap = wx.StaticBitmap(
  97. infoTxt, wx.ID_ANY, wx.Bitmap(name=logo, type=wx.BITMAP_TYPE_PNG)
  98. )
  99. infoSizer.Add(
  100. logoBitmap, proportion=0, flag=wx.ALL | wx.ALIGN_CENTER, border=20
  101. )
  102. infoLabel = "GRASS GIS %s" % vInfo.get("version", _("unknown version"))
  103. if "x86_64" in vInfo.get("build_platform", ""):
  104. infoLabel += " (64bit)"
  105. info = StaticText(parent=infoTxt, id=wx.ID_ANY, label=infoLabel + os.linesep)
  106. info.SetFont(wx.Font(13, wx.DEFAULT, wx.NORMAL, wx.BOLD, 0, ""))
  107. info.SetForegroundColour(wx.Colour(35, 142, 35))
  108. infoSizer.Add(info, proportion=0, flag=wx.BOTTOM | wx.ALIGN_CENTER, border=1)
  109. team = StaticText(parent=infoTxt, label=_grassDevTeam(1999) + "\n")
  110. infoSizer.Add(team, proportion=0, flag=wx.BOTTOM | wx.ALIGN_CENTER, border=1)
  111. row = 0
  112. infoGridSizer.Add(
  113. StaticText(parent=infoTxt, id=wx.ID_ANY, label=_("Official GRASS site:")),
  114. pos=(row, 0),
  115. flag=wx.ALIGN_RIGHT,
  116. )
  117. infoGridSizer.Add(
  118. HyperLinkCtrl(
  119. parent=infoTxt, id=wx.ID_ANY, label="https://grass.osgeo.org"
  120. ),
  121. pos=(row, 1),
  122. flag=wx.ALIGN_LEFT,
  123. )
  124. row += 2
  125. infoGridSizer.Add(
  126. StaticText(parent=infoTxt, id=wx.ID_ANY, label="%s:" % _("Code Revision")),
  127. pos=(row, 0),
  128. flag=wx.ALIGN_RIGHT,
  129. )
  130. infoGridSizer.Add(
  131. HyperLinkCtrl(
  132. parent=infoTxt,
  133. id=wx.ID_ANY,
  134. label=vInfo.get("revision", "?"),
  135. URL="https://github.com/OSGeo/grass.git",
  136. ),
  137. pos=(row, 1),
  138. flag=wx.ALIGN_LEFT,
  139. )
  140. row += 1
  141. infoGridSizer.Add(
  142. StaticText(parent=infoTxt, id=wx.ID_ANY, label="%s:" % _("Build Date")),
  143. pos=(row, 0),
  144. flag=wx.ALIGN_RIGHT,
  145. )
  146. infoGridSizer.Add(
  147. StaticText(
  148. parent=infoTxt, id=wx.ID_ANY, label=vInfo.get("build_date", "?")
  149. ),
  150. pos=(row, 1),
  151. flag=wx.ALIGN_LEFT,
  152. )
  153. # show only basic info
  154. # row += 1
  155. # infoGridSizer.Add(item = wx.StaticText(parent = infoTxt, id = wx.ID_ANY,
  156. # label = '%s:' % _('GIS Library Revision')),
  157. # pos = (row, 0),
  158. # flag = wx.ALIGN_RIGHT)
  159. # infoGridSizer.Add(item = wx.StaticText(parent = infoTxt, id = wx.ID_ANY,
  160. # label = vInfo['libgis_revision'] + ' (' +
  161. # vInfo['libgis_date'].split(' ')[0] + ')'),
  162. # pos = (row, 1),
  163. # flag = wx.ALIGN_LEFT)
  164. row += 2
  165. infoGridSizer.Add(
  166. StaticText(parent=infoTxt, id=wx.ID_ANY, label="Python:"),
  167. pos=(row, 0),
  168. flag=wx.ALIGN_RIGHT,
  169. )
  170. infoGridSizer.Add(
  171. StaticText(parent=infoTxt, id=wx.ID_ANY, label=platform.python_version()),
  172. pos=(row, 1),
  173. flag=wx.ALIGN_LEFT,
  174. )
  175. row += 1
  176. infoGridSizer.Add(
  177. StaticText(parent=infoTxt, id=wx.ID_ANY, label="wxPython:"),
  178. pos=(row, 0),
  179. flag=wx.ALIGN_RIGHT,
  180. )
  181. infoGridSizer.Add(
  182. StaticText(parent=infoTxt, id=wx.ID_ANY, label=wx.__version__),
  183. pos=(row, 1),
  184. flag=wx.ALIGN_LEFT,
  185. )
  186. infoGridSizer.AddGrowableCol(0)
  187. infoGridSizer.AddGrowableCol(1)
  188. infoSizer.Add(infoGridSizer, proportion=1, flag=wx.EXPAND)
  189. row += 2
  190. infoGridSizer.Add(
  191. StaticText(parent=infoTxt, id=wx.ID_ANY, label="%s:" % _("Language")),
  192. pos=(row, 0),
  193. flag=wx.ALIGN_RIGHT,
  194. )
  195. self.langUsed = grass.gisenv().get("LANG", None)
  196. if not self.langUsed:
  197. import locale
  198. loc = locale.getdefaultlocale()
  199. if loc == (None, None):
  200. self.langUsed = _("unknown")
  201. else:
  202. self.langUsed = "%s.%s" % (loc[0], loc[1])
  203. infoGridSizer.Add(
  204. StaticText(parent=infoTxt, id=wx.ID_ANY, label=self.langUsed),
  205. pos=(row, 1),
  206. flag=wx.ALIGN_LEFT,
  207. )
  208. infoTxt.SetSizer(infoSizer)
  209. infoSizer.Fit(infoTxt)
  210. return infoTxt
  211. def _pageCopyright(self):
  212. """Copyright information"""
  213. copyfile = os.path.join(os.getenv("GISBASE"), "COPYING")
  214. if os.path.exists(copyfile):
  215. copyrightFile = open(copyfile, "r")
  216. copytext = copyrightFile.read()
  217. copyrightFile.close()
  218. else:
  219. copytext = _("%s file missing") % "COPYING"
  220. # put text into a scrolling panel
  221. copyrightwin = ScrolledPanel(self.aboutNotebook)
  222. copyrighttxt = TextCtrl(
  223. copyrightwin,
  224. id=wx.ID_ANY,
  225. value=copytext,
  226. style=wx.TE_MULTILINE | wx.TE_READONLY,
  227. )
  228. copyrightwin.SetAutoLayout(True)
  229. copyrightwin.sizer = wx.BoxSizer(wx.VERTICAL)
  230. copyrightwin.sizer.Add(
  231. copyrighttxt, proportion=1, flag=wx.EXPAND | wx.ALL, border=3
  232. )
  233. copyrightwin.SetSizer(copyrightwin.sizer)
  234. copyrightwin.Layout()
  235. copyrightwin.SetupScrolling()
  236. return copyrightwin
  237. def _pageLicense(self):
  238. """Licence about"""
  239. licfile = os.path.join(os.getenv("GISBASE"), "GPL.TXT")
  240. if os.path.exists(licfile):
  241. licenceFile = open(licfile, "r")
  242. license = "".join(licenceFile.readlines())
  243. licenceFile.close()
  244. else:
  245. license = _("%s file missing") % "GPL.TXT"
  246. # put text into a scrolling panel
  247. licensewin = ScrolledPanel(self.aboutNotebook)
  248. licensetxt = TextCtrl(
  249. licensewin,
  250. id=wx.ID_ANY,
  251. value=license,
  252. style=wx.TE_MULTILINE | wx.TE_READONLY,
  253. )
  254. licensewin.SetAutoLayout(True)
  255. licensewin.sizer = wx.BoxSizer(wx.VERTICAL)
  256. licensewin.sizer.Add(
  257. licensetxt, proportion=1, flag=wx.EXPAND | wx.ALL, border=3
  258. )
  259. licensewin.SetSizer(licensewin.sizer)
  260. licensewin.Layout()
  261. licensewin.SetupScrolling()
  262. return licensewin
  263. def _pageCitation(self):
  264. """Citation information"""
  265. try:
  266. # import only when needed
  267. import grass.script as gscript
  268. text = gscript.read_command("g.version", flags="x")
  269. except CalledModuleError as error:
  270. text = _(
  271. "Unable to provide citation suggestion,"
  272. " see GRASS GIS website instead."
  273. " The error was: {0}"
  274. ).format(error)
  275. # put text into a scrolling panel
  276. window = ScrolledPanel(self.aboutNotebook)
  277. stat_text = TextCtrl(
  278. window, id=wx.ID_ANY, value=text, style=wx.TE_MULTILINE | wx.TE_READONLY
  279. )
  280. window.SetAutoLayout(True)
  281. window.sizer = wx.BoxSizer(wx.VERTICAL)
  282. window.sizer.Add(stat_text, proportion=1, flag=wx.EXPAND | wx.ALL, border=3)
  283. window.SetSizer(window.sizer)
  284. window.Layout()
  285. window.SetupScrolling()
  286. return window
  287. def _pageCredit(self):
  288. """Credit about"""
  289. # credits
  290. authfile = os.path.join(os.getenv("GISBASE"), "AUTHORS")
  291. if os.path.exists(authfile):
  292. with codecs.open(authfile, encoding="utf-8", mode="r") as authorsFile:
  293. authors = "".join(authorsFile.readlines())
  294. else:
  295. authors = _("%s file missing") % "AUTHORS"
  296. authorwin = ScrolledPanel(self.aboutNotebook)
  297. authortxt = TextCtrl(
  298. authorwin,
  299. id=wx.ID_ANY,
  300. value=authors,
  301. style=wx.TE_MULTILINE | wx.TE_READONLY,
  302. )
  303. authorwin.SetAutoLayout(True)
  304. authorwin.SetupScrolling()
  305. authorwin.sizer = wx.BoxSizer(wx.VERTICAL)
  306. authorwin.sizer.Add(authortxt, proportion=1, flag=wx.EXPAND | wx.ALL, border=3)
  307. authorwin.SetSizer(authorwin.sizer)
  308. authorwin.Layout()
  309. return authorwin
  310. def _pageContributors(self, extra=False):
  311. """Contributors info"""
  312. if extra:
  313. contribfile = os.path.join(os.getenv("GISBASE"), "contributors_extra.csv")
  314. else:
  315. contribfile = os.path.join(os.getenv("GISBASE"), "contributors.csv")
  316. if os.path.exists(contribfile):
  317. contribFile = codecs.open(contribfile, encoding="utf-8", mode="r")
  318. contribs = list()
  319. errLines = list()
  320. for line in contribFile.readlines()[1:]:
  321. line = line.rstrip("\n")
  322. try:
  323. if extra:
  324. name, email, country, rfc2_agreed = line.split(",")
  325. else:
  326. (
  327. cvs_id,
  328. name,
  329. email,
  330. country,
  331. osgeo_id,
  332. rfc2_agreed,
  333. orcid,
  334. ) = line.split(",")
  335. except ValueError:
  336. errLines.append(line)
  337. continue
  338. if extra:
  339. contribs.append((name, email, country))
  340. else:
  341. contribs.append((name, email, country, osgeo_id, orcid))
  342. contribFile.close()
  343. if errLines:
  344. GError(
  345. parent=self,
  346. message=_("Error when reading file '%s'.") % contribfile
  347. + "\n\n"
  348. + _("Lines:")
  349. + " %s" % os.linesep.join(map(DecodeString, errLines)),
  350. )
  351. else:
  352. contribs = None
  353. contribwin = ScrolledPanel(self.aboutNotebook)
  354. contribwin.SetAutoLayout(True)
  355. contribwin.SetupScrolling()
  356. contribwin.sizer = wx.BoxSizer(wx.VERTICAL)
  357. if not contribs:
  358. contribtxt = StaticText(
  359. contribwin, id=wx.ID_ANY, label=_("%s file missing") % contribfile
  360. )
  361. contribwin.sizer.Add(
  362. contribtxt, proportion=1, flag=wx.EXPAND | wx.ALL, border=3
  363. )
  364. else:
  365. if extra:
  366. items = (_("Name"), _("E-mail"), _("Country"))
  367. else:
  368. items = (
  369. _("Name"),
  370. _("E-mail"),
  371. _("Country"),
  372. _("OSGeo_ID"),
  373. _("ORCID"),
  374. )
  375. contribBox = wx.FlexGridSizer(cols=len(items), vgap=5, hgap=5)
  376. for item in items:
  377. text = StaticText(parent=contribwin, id=wx.ID_ANY, label=item)
  378. text.SetFont(wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.BOLD, 0, ""))
  379. contribBox.Add(text)
  380. for vals in sorted(contribs, key=lambda x: x[0]):
  381. for item in vals:
  382. contribBox.Add(
  383. StaticText(parent=contribwin, id=wx.ID_ANY, label=item)
  384. )
  385. contribwin.sizer.Add(
  386. contribBox, proportion=1, flag=wx.EXPAND | wx.ALL, border=3
  387. )
  388. contribwin.SetSizer(contribwin.sizer)
  389. contribwin.Layout()
  390. return contribwin
  391. def _pageTranslators(self):
  392. """Translators info"""
  393. translatorsfile = os.path.join(os.getenv("GISBASE"), "translators.csv")
  394. if os.path.exists(translatorsfile):
  395. translatorsFile = codecs.open(translatorsfile, encoding="utf-8", mode="r")
  396. translators = dict()
  397. errLines = list()
  398. for line in translatorsFile.readlines()[1:]:
  399. line = line.rstrip("\n")
  400. try:
  401. name, email, languages = line.split(",")
  402. except ValueError:
  403. errLines.append(line)
  404. continue
  405. for language in languages.split(" "):
  406. if language not in translators:
  407. translators[language] = list()
  408. translators[language].append((name, email))
  409. translatorsFile.close()
  410. if errLines:
  411. GError(
  412. parent=self,
  413. message=_("Error when reading file '%s'.") % translatorsfile
  414. + "\n\n"
  415. + _("Lines:")
  416. + " %s" % os.linesep.join(map(DecodeString, errLines)),
  417. )
  418. else:
  419. translators = None
  420. translatorswin = ScrolledPanel(self.aboutNotebook)
  421. translatorswin.SetAutoLayout(True)
  422. translatorswin.SetupScrolling()
  423. translatorswin.sizer = wx.BoxSizer(wx.VERTICAL)
  424. if not translators:
  425. translatorstxt = StaticText(
  426. translatorswin,
  427. id=wx.ID_ANY,
  428. label=_("%s file missing") % "translators.csv",
  429. )
  430. translatorswin.sizer.Add(
  431. translatorstxt, proportion=1, flag=wx.EXPAND | wx.ALL, border=3
  432. )
  433. else:
  434. translatorsBox = wx.FlexGridSizer(cols=4, vgap=5, hgap=5)
  435. languages = sorted(translators.keys())
  436. tname = StaticText(parent=translatorswin, id=wx.ID_ANY, label=_("Name"))
  437. tname.SetFont(wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.BOLD, 0, ""))
  438. translatorsBox.Add(tname)
  439. temail = StaticText(parent=translatorswin, id=wx.ID_ANY, label=_("E-mail"))
  440. temail.SetFont(wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.BOLD, 0, ""))
  441. translatorsBox.Add(temail)
  442. tlang = StaticText(parent=translatorswin, id=wx.ID_ANY, label=_("Language"))
  443. tlang.SetFont(wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.BOLD, 0, ""))
  444. translatorsBox.Add(tlang)
  445. tnat = StaticText(parent=translatorswin, id=wx.ID_ANY, label=_("Nation"))
  446. tnat.SetFont(wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.BOLD, 0, ""))
  447. translatorsBox.Add(tnat)
  448. for lang in languages:
  449. for translator in translators[lang]:
  450. name, email = translator
  451. translatorsBox.Add(
  452. StaticText(parent=translatorswin, id=wx.ID_ANY, label=name)
  453. )
  454. translatorsBox.Add(
  455. StaticText(parent=translatorswin, id=wx.ID_ANY, label=email)
  456. )
  457. translatorsBox.Add(
  458. StaticText(parent=translatorswin, id=wx.ID_ANY, label=lang)
  459. )
  460. flag = os.path.join(
  461. globalvar.ICONDIR, "flags", "%s.png" % lang.lower()
  462. )
  463. if os.path.exists(flag):
  464. flagBitmap = wx.StaticBitmap(
  465. translatorswin,
  466. wx.ID_ANY,
  467. wx.Bitmap(name=flag, type=wx.BITMAP_TYPE_PNG),
  468. )
  469. translatorsBox.Add(flagBitmap)
  470. else:
  471. translatorsBox.Add(
  472. StaticText(parent=translatorswin, id=wx.ID_ANY, label=lang)
  473. )
  474. translatorswin.sizer.Add(
  475. translatorsBox, proportion=1, flag=wx.EXPAND | wx.ALL, border=3
  476. )
  477. translatorswin.SetSizer(translatorswin.sizer)
  478. translatorswin.Layout()
  479. return translatorswin
  480. def _langString(self, k, v):
  481. """Return string for the status of translation"""
  482. allStr = "%s :" % k.upper()
  483. try:
  484. allStr += _(" %d translated" % v["good"])
  485. except:
  486. pass
  487. try:
  488. allStr += _(" %d fuzzy" % v["fuzzy"])
  489. except:
  490. pass
  491. try:
  492. allStr += _(" %d untranslated" % v["bad"])
  493. except:
  494. pass
  495. return allStr
  496. def _langBox(self, par, k, v):
  497. """Return box"""
  498. langBox = wx.FlexGridSizer(cols=4, vgap=5, hgap=5)
  499. tkey = StaticText(parent=par, id=wx.ID_ANY, label=k.upper())
  500. langBox.Add(tkey)
  501. try:
  502. tgood = StaticText(
  503. parent=par, id=wx.ID_ANY, label=_("%d translated" % v["good"])
  504. )
  505. tgood.SetForegroundColour(wx.Colour(35, 142, 35))
  506. langBox.Add(tgood)
  507. except:
  508. tgood = StaticText(parent=par, id=wx.ID_ANY, label="")
  509. langBox.Add(tgood)
  510. try:
  511. tfuzzy = StaticText(
  512. parent=par, id=wx.ID_ANY, label=_(" %d fuzzy" % v["fuzzy"])
  513. )
  514. tfuzzy.SetForegroundColour(wx.Colour(255, 142, 0))
  515. langBox.Add(tfuzzy)
  516. except:
  517. tfuzzy = StaticText(parent=par, id=wx.ID_ANY, label="")
  518. langBox.Add(tfuzzy)
  519. try:
  520. tbad = StaticText(
  521. parent=par, id=wx.ID_ANY, label=_(" %d untranslated" % v["bad"])
  522. )
  523. tbad.SetForegroundColour(wx.Colour(255, 0, 0))
  524. langBox.Add(tbad)
  525. except:
  526. tbad = StaticText(parent=par, id=wx.ID_ANY, label="")
  527. langBox.Add(tbad)
  528. return langBox
  529. def _langPanel(self, lang, js):
  530. """Create panel for each languages"""
  531. text = self._langString(lang, js["total"])
  532. panel = wx.CollapsiblePane(
  533. self.statswin,
  534. -1,
  535. label=text,
  536. style=wx.CP_DEFAULT_STYLE | wx.CP_NO_TLW_RESIZE,
  537. )
  538. panel.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED, self.OnPaneChanged)
  539. win = panel.GetPane()
  540. # TODO IT DOESN'T WORK
  541. # TO ADD ONLY WHEN TAB IS OPENED
  542. # if lang == self.langUsed.split('_')[0]:
  543. # panel.Collapse(False)
  544. # else:
  545. # panel.Collapse(True)
  546. pageSizer = wx.BoxSizer(wx.VERTICAL)
  547. for k, v in six.iteritems(js):
  548. if k != "total" and k != "name":
  549. box = self._langBox(win, k, v)
  550. pageSizer.Add(box, proportion=1, flag=wx.EXPAND | wx.ALL, border=3)
  551. win.SetSizer(pageSizer)
  552. pageSizer.SetSizeHints(win)
  553. return panel
  554. def OnPaneChanged(self, evt):
  555. """Redo the layout"""
  556. # TODO better to test on Windows
  557. self.statswin.SetupScrolling(scrollToTop=False)
  558. def _pageStats(self):
  559. """Translation statistics info"""
  560. fname = "translation_status.json"
  561. statsfile = os.path.join(os.getenv("GISBASE"), fname)
  562. if os.path.exists(statsfile):
  563. statsFile = open(statsfile)
  564. import json
  565. jsStats = json.load(statsFile)
  566. else:
  567. jsStats = None
  568. self.statswin = ScrolledPanel(self.aboutNotebook)
  569. self.statswin.SetAutoLayout(True)
  570. if not jsStats:
  571. Debug.msg(5, _("File <%s> not found") % fname)
  572. statsSizer = wx.BoxSizer(wx.VERTICAL)
  573. statstext = StaticText(
  574. self.statswin, id=wx.ID_ANY, label=_("%s file missing") % fname
  575. )
  576. statsSizer.Add(statstext, proportion=1, flag=wx.EXPAND | wx.ALL, border=3)
  577. else:
  578. languages = sorted(jsStats["langs"].keys())
  579. statsSizer = wx.BoxSizer(wx.VERTICAL)
  580. for lang in languages:
  581. v = jsStats["langs"][lang]
  582. panel = self._langPanel(lang, v)
  583. statsSizer.Add(panel)
  584. self.statswin.SetSizer(statsSizer)
  585. self.statswin.SetupScrolling(scroll_x=False, scroll_y=True)
  586. self.statswin.Layout()
  587. self.statswin.Fit()
  588. return self.statswin
  589. def OnCloseWindow(self, event):
  590. """Close window"""
  591. self.Close()
  592. class HelpFrame(wx.Dialog):
  593. """GRASS Quickstart help window
  594. As a base class wx.Dialog is used, because of not working
  595. close button with wx.Frame when dialog is called from wizard.
  596. If parent is None, application TopLevelWindow is used (wxPython
  597. standard behaviour).
  598. Currently not used (was in location wizard before)
  599. due to unsolved problems - window sometimes does not respond.
  600. """
  601. def __init__(self, parent, id, title, size, file):
  602. wx.Dialog.__init__(
  603. self,
  604. parent=parent,
  605. id=id,
  606. title=title,
  607. size=size,
  608. style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.MINIMIZE_BOX,
  609. )
  610. sizer = wx.BoxSizer(wx.VERTICAL)
  611. # text
  612. content = HelpPanel(parent=self)
  613. content.LoadPage(file)
  614. sizer.Add(content, proportion=1, flag=wx.EXPAND)
  615. self.SetAutoLayout(True)
  616. self.SetSizer(sizer)
  617. self.Layout()
  618. class HelpWindow(HtmlWindow):
  619. """This panel holds the text from GRASS docs.
  620. GISBASE must be set in the environment to find the html docs dir.
  621. The SYNOPSIS section is skipped, since this Panel is supposed to
  622. be integrated into the cmdPanel and options are obvious there.
  623. """
  624. def __init__(self, parent, command, text, skipDescription, **kwargs):
  625. """If command is given, the corresponding HTML help
  626. file will be presented, with all links pointing to absolute
  627. paths of local files.
  628. If 'skipDescription' is True, the HTML corresponding to
  629. SYNOPSIS will be skipped, thus only presenting the help file
  630. from the DESCRIPTION section onwards.
  631. If 'text' is given, it must be the HTML text to be presented
  632. in the Panel.
  633. """
  634. self.parent = parent
  635. HtmlWindow.__init__(self, parent=parent, **kwargs)
  636. self.loaded = False
  637. self.history = list()
  638. self.historyIdx = 0
  639. self.fspath = os.path.join(os.getenv("GISBASE"), "docs", "html")
  640. self.SetStandardFonts(size=10)
  641. self.SetBorders(10)
  642. if text is None:
  643. if skipDescription:
  644. url = os.path.join(self.fspath, command + ".html")
  645. self.fillContentsFromFile(url, skipDescription=skipDescription)
  646. self.history.append(url)
  647. self.loaded = True
  648. else:
  649. # FIXME: calling LoadPage() is strangely time-consuming (only first call)
  650. # self.LoadPage(self.fspath + command + ".html")
  651. self.loaded = False
  652. else:
  653. self.SetPage(text)
  654. self.loaded = True
  655. def OnLinkClicked(self, linkinfo):
  656. url = linkinfo.GetHref()
  657. if url[:4] != "http":
  658. url = os.path.join(self.fspath, url)
  659. self.history.append(url)
  660. self.historyIdx += 1
  661. self.parent.OnHistory()
  662. super(HelpWindow, self).OnLinkClicked(linkinfo)
  663. def LoadPage(self, path):
  664. super(HelpWindow, self).LoadPage(path)
  665. self.loaded = True
  666. def fillContentsFromFile(self, htmlFile, skipDescription=True):
  667. """Load content from file.
  668. Currently not used.
  669. """
  670. aLink = re.compile(r'(<a href="?)(.+\.html?["\s]*>)', re.IGNORECASE)
  671. imgLink = re.compile(r'(<img src="?)(.+\.[png|gif])', re.IGNORECASE)
  672. try:
  673. contents = []
  674. skip = False
  675. for line in open(htmlFile, "rb").readlines():
  676. if "DESCRIPTION" in line:
  677. skip = False
  678. if not skip:
  679. # do skip the options description if requested
  680. if "SYNOPSIS" in line:
  681. skip = skipDescription
  682. else:
  683. # FIXME: find only first item
  684. findALink = aLink.search(line)
  685. if findALink is not None:
  686. contents.append(
  687. aLink.sub(
  688. findALink.group(1)
  689. + self.fspath
  690. + findALink.group(2),
  691. line,
  692. )
  693. )
  694. findImgLink = imgLink.search(line)
  695. if findImgLink is not None:
  696. contents.append(
  697. imgLink.sub(
  698. findImgLink.group(1)
  699. + self.fspath
  700. + findImgLink.group(2),
  701. line,
  702. )
  703. )
  704. if findALink is None and findImgLink is None:
  705. contents.append(line)
  706. self.SetPage("".join(contents))
  707. self.loaded = True
  708. except: # The Manual file was not found
  709. self.loaded = False
  710. class HelpPanel(wx.Panel):
  711. def __init__(
  712. self, parent, command="index", text=None, skipDescription=False, **kwargs
  713. ):
  714. self.command = command
  715. wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY)
  716. self.content = HelpWindow(self, command, text, skipDescription)
  717. self.btnNext = Button(parent=self, id=wx.ID_ANY, label=_("&Next"))
  718. self.btnNext.Enable(False)
  719. self.btnPrev = Button(parent=self, id=wx.ID_ANY, label=_("&Previous"))
  720. self.btnPrev.Enable(False)
  721. self.btnNext.Bind(wx.EVT_BUTTON, self.OnNext)
  722. self.btnPrev.Bind(wx.EVT_BUTTON, self.OnPrev)
  723. self._layout()
  724. def _layout(self):
  725. """Do layout"""
  726. sizer = wx.BoxSizer(wx.VERTICAL)
  727. btnSizer = wx.BoxSizer(wx.HORIZONTAL)
  728. btnSizer.Add(self.btnPrev, proportion=0, flag=wx.ALL, border=5)
  729. btnSizer.Add(wx.Size(1, 1), proportion=1)
  730. btnSizer.Add(self.btnNext, proportion=0, flag=wx.ALL, border=5)
  731. sizer.Add(self.content, proportion=1, flag=wx.EXPAND)
  732. sizer.Add(btnSizer, proportion=0, flag=wx.EXPAND)
  733. self.SetSizer(sizer)
  734. sizer.Fit(self)
  735. def LoadPage(self, path=None):
  736. """Load page"""
  737. if not path:
  738. path = self.GetFile()
  739. self.content.history.append(path)
  740. self.content.LoadPage(path)
  741. def GetFile(self):
  742. """Get HTML file"""
  743. fMan = os.path.join(self.content.fspath, self.command + ".html")
  744. if os.path.isfile(fMan):
  745. return fMan
  746. # check also addons
  747. faMan = os.path.join(
  748. os.getenv("GRASS_ADDON_BASE"), "docs", "html", self.command + ".html"
  749. )
  750. if os.getenv("GRASS_ADDON_BASE") and os.path.isfile(faMan):
  751. return faMan
  752. return None
  753. def IsLoaded(self):
  754. return self.content.loaded
  755. def OnHistory(self):
  756. """Update buttons"""
  757. nH = len(self.content.history)
  758. iH = self.content.historyIdx
  759. if iH == nH - 1:
  760. self.btnNext.Enable(False)
  761. elif iH > -1:
  762. self.btnNext.Enable(True)
  763. if iH < 1:
  764. self.btnPrev.Enable(False)
  765. else:
  766. self.btnPrev.Enable(True)
  767. def OnNext(self, event):
  768. """Load next page"""
  769. self.content.historyIdx += 1
  770. idx = self.content.historyIdx
  771. path = self.content.history[idx]
  772. self.content.LoadPage(path)
  773. self.OnHistory()
  774. event.Skip()
  775. def OnPrev(self, event):
  776. """Load previous page"""
  777. self.content.historyIdx -= 1
  778. idx = self.content.historyIdx
  779. path = self.content.history[idx]
  780. self.content.LoadPage(path)
  781. self.OnHistory()
  782. event.Skip()
  783. def ShowAboutDialog(prgName, startYear):
  784. """Displays About window.
  785. :param prgName: name of the program
  786. :param startYear: the first year of existence of the program
  787. """
  788. info = AboutDialogInfo()
  789. info.SetIcon(
  790. wx.Icon(os.path.join(globalvar.ICONDIR, "grass.ico"), wx.BITMAP_TYPE_ICO)
  791. )
  792. info.SetName(prgName)
  793. info.SetWebSite("https://grass.osgeo.org")
  794. info.SetDescription(
  795. _grassDevTeam(startYear)
  796. + "\n\n"
  797. + "\n".join(
  798. textwrap.wrap(
  799. "This program is free software under the GNU General Public License"
  800. "(>=v2). Read the file COPYING that comes with GRASS for details.",
  801. 75,
  802. )
  803. )
  804. )
  805. AboutBox(info)
  806. def _grassDevTeam(start):
  807. try:
  808. end = grass.version()["date"]
  809. except KeyError:
  810. sys.stderr.write(_("Unable to get GRASS version\n"))
  811. from datetime import date
  812. end = date.today().year
  813. return "%(c)s %(start)s-%(end)s by the GRASS Development Team" % {
  814. "c": chr(169),
  815. "start": start,
  816. "end": end,
  817. }
  818. def main():
  819. """Test application (potentially useful as g.gui.gmanual)"""
  820. app = wx.App()
  821. frame = HelpFrame(
  822. parent=None,
  823. id=wx.ID_ANY,
  824. title="Test help application",
  825. size=(600, 800),
  826. file=sys.argv[1],
  827. )
  828. frame.Show()
  829. app.MainLoop()
  830. if __name__ == "__main__":
  831. main()