You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1826 lines
62 KiB

  1. # a graphical (GTK+) user interface
  2. # Written by Luca Bruno <lethalman88@gmail.com>
  3. # Based on gnome-reportbug work done by Philipp Kern <pkern@debian.org>
  4. # Copyright (C) 2006 Philipp Kern
  5. # Copyright (C) 2008-2009 Luca Bruno
  6. #
  7. # This program is freely distributable per the following license:
  8. #
  9. # Permission to use, copy, modify, and distribute this software and its
  10. # documentation for any purpose and without fee is hereby granted,
  11. # provided that the above copyright notice appears in all copies and that
  12. # both that copyright notice and this permission notice appear in
  13. # supporting documentation.
  14. #
  15. # I DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
  16. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL I
  17. # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
  18. # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
  19. # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
  20. # ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
  21. # SOFTWARE.
  22. from reportbug.exceptions import UINotImportable
  23. try:
  24. import gi
  25. gi.require_version('GLib', '2.0')
  26. from gi.repository import GLib
  27. gi.require_version('GObject', '2.0')
  28. from gi.repository import GObject
  29. gi.require_version('Pango', '1.0')
  30. from gi.repository import Pango
  31. gi.require_version('Gdk', '3.0')
  32. from gi.repository import Gdk
  33. gi.require_version('GdkPixbuf', '2.0')
  34. from gi.repository import GdkPixbuf
  35. gi.require_version('Gtk', '3.0')
  36. from gi.repository import Gtk
  37. except ImportError:
  38. raise UINotImportable('Please install the python3-gi and gir1.2-gtk-3.0 packages to use this interface.')
  39. global Vte
  40. gtkspellcheck = None
  41. import sys
  42. import re
  43. import os
  44. import traceback
  45. from queue import Queue
  46. import threading
  47. import textwrap
  48. from reportbug.exceptions import NoPackage, NoBugs, NoNetwork, NoReport
  49. from reportbug import debbugs
  50. from reportbug.urlutils import launch_browser
  51. ISATTY = True
  52. DEBIAN_LOGO = "/usr/share/pixmaps/debian-logo.png"
  53. global application, assistant, report_message, reportbug_context, ui_context
  54. # Utilities
  55. def _describe_context(context):
  56. if context == ui_context:
  57. return '<MainContext of UI thread>'
  58. elif context == reportbug_context:
  59. return '<MainContext of reportbug thread>'
  60. else:
  61. return repr(context)
  62. def _assert_context(expected):
  63. really = GLib.MainContext.ref_thread_default()
  64. # This compares by pointer value of the underlying GMainContext
  65. if really != expected:
  66. raise AssertionError('Function should be called in %s but was called in %s' %
  67. (_describe_context(really), _describe_context(expected)))
  68. if not really.is_owner():
  69. raise AssertionError('Function should be called with %s acquired')
  70. def highlight(s):
  71. return '<b>%s</b>' % s
  72. re_markup_free = re.compile("<.*?>")
  73. def markup_free(s):
  74. return re_markup_free.sub("", s)
  75. def ask_free(s):
  76. s = s.strip()
  77. if s[-1] in('?', ':'):
  78. return s[:-1]
  79. return s
  80. def create_scrollable(widget, with_viewport=False):
  81. _assert_context(ui_context)
  82. scrolled = Gtk.ScrolledWindow()
  83. scrolled.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
  84. scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
  85. if with_viewport:
  86. scrolled.add_with_viewport(widget)
  87. else:
  88. scrolled.add(widget)
  89. return scrolled
  90. def info_dialog(message):
  91. _assert_context(ui_context)
  92. dialog = Gtk.MessageDialog(assistant, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
  93. Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, message)
  94. dialog.connect('response', lambda d, *args: d.destroy())
  95. dialog.set_title('Reportbug')
  96. dialog.show_all()
  97. def error_dialog(message):
  98. _assert_context(ui_context)
  99. dialog = Gtk.MessageDialog(assistant, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
  100. Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, message)
  101. dialog.connect('response', lambda d, *args: d.destroy())
  102. dialog.set_title('Reportbug')
  103. dialog.show_all()
  104. class CustomDialog(Gtk.Dialog):
  105. def __init__(self, stock_image, message, buttons, *args, **kwargs):
  106. _assert_context(ui_context)
  107. Gtk.Dialog.__init__(self, "Reportbug", assistant,
  108. Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
  109. buttons)
  110. # Try following the HIG
  111. self.set_default_response(buttons[-1]) # this is the response of the last button
  112. self.set_border_width(5)
  113. vbox = Gtk.VBox(spacing=10)
  114. vbox.set_border_width(6)
  115. self.vbox.pack_start(vbox, True, True, 0)
  116. # The header image + label
  117. hbox = Gtk.HBox(spacing=10)
  118. vbox.pack_start(hbox, False, True, 0)
  119. # TODO: deprecated, new code is meant to set the halign/valign/margin
  120. # properties on the child widget instead. Also this is probably
  121. # useless without having a child widget?
  122. align = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=1.0, yscale=1.0)
  123. hbox.pack_start(align, False, True, 0)
  124. image = Gtk.Image.new_from_stock(stock_image, Gtk.IconSize.DIALOG)
  125. hbox.pack_start(image, True, True, 0)
  126. label = Gtk.Label(label=message)
  127. label.set_line_wrap(True)
  128. label.set_justify(Gtk.Justification.FILL)
  129. label.set_selectable(True)
  130. label.set_property("can-focus", False)
  131. hbox.pack_start(label, False, True, 0)
  132. self.setup_dialog(vbox, *args, **kwargs)
  133. class InputStringDialog(CustomDialog):
  134. def __init__(self, message):
  135. _assert_context(ui_context)
  136. CustomDialog.__init__(self, Gtk.STOCK_DIALOG_INFO, message,
  137. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  138. Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT))
  139. def setup_dialog(self, vbox):
  140. _assert_context(ui_context)
  141. self.entry = Gtk.Entry()
  142. vbox.pack_start(self.entry, False, True, 0)
  143. def get_value(self):
  144. _assert_context(ui_context)
  145. return self.entry.get_text()
  146. class ExceptionDialog(CustomDialog):
  147. # Register an exception hook to display an error when the GUI breaks
  148. @classmethod
  149. def create_excepthook(cls, oldhook):
  150. _assert_context(reportbug_context)
  151. def excepthook(exctype, value, tb):
  152. # OK to call from any thread
  153. if oldhook:
  154. oldhook(exctype, value, tb)
  155. application.run_once_in_main_thread(cls.start_dialog,
  156. ''.join(traceback.format_exception(exctype, value, tb)))
  157. return excepthook
  158. @classmethod
  159. def start_dialog(cls, tb):
  160. _assert_context(ui_context)
  161. try:
  162. dialog = cls(tb)
  163. dialog.show_all()
  164. except:
  165. sys.exit(1)
  166. def __init__(self, tb):
  167. _assert_context(ui_context)
  168. CustomDialog.__init__(self, Gtk.STOCK_DIALOG_ERROR, "An error has occurred while doing an operation in Reportbug.\nPlease report the bug.", (Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE), tb)
  169. def setup_dialog(self, vbox, tb):
  170. # The traceback
  171. expander = Gtk.Expander.new_with_mnemonic("More details")
  172. vbox.pack_start(expander, True, True, 0)
  173. view = Gtk.TextView()
  174. view.set_editable(False)
  175. view.get_buffer().set_text(tb)
  176. scrolled = create_scrollable(view)
  177. expander.add(scrolled)
  178. self.connect('response', self.on_response)
  179. def on_response(self, dialog, res):
  180. _assert_context(ui_context)
  181. sys.exit(1)
  182. class ReportViewerDialog(Gtk.Dialog):
  183. def __init__(self, message):
  184. _assert_context(ui_context)
  185. Gtk.Dialog.__init__(self, "Reportbug", assistant,
  186. Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
  187. (Gtk.STOCK_COPY, Gtk.ResponseType.APPLY,
  188. Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE))
  189. self.message = message
  190. self.set_default_size(400, 400)
  191. self.set_default_response(Gtk.ResponseType.CLOSE)
  192. self.set_border_width(6)
  193. self.connect('response', self.on_response)
  194. view = Gtk.TextView()
  195. view.get_buffer().set_text(self.message)
  196. self.vbox.pack_start(create_scrollable(view), True, True, 0)
  197. self.show_all()
  198. def on_response(self, dialog, res):
  199. _assert_context(ui_context)
  200. # ok Gtk.ResponseType.APPLY is ugly for Gtk.STOCK_COPY, but who cares?
  201. # maybe adding it as a secondary button or such is better
  202. if res == Gtk.ResponseType.APPLY:
  203. clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
  204. clipboard.set_text(self.message)
  205. else:
  206. self.destroy()
  207. # BTS
  208. class Bug(object):
  209. """Encapsulate a bug report for the GTK+ UI"""
  210. def __init__(self, bug):
  211. self.id = bug.bug_num
  212. self.tag = ', '.join(bug.tags)
  213. self.package = bug.package
  214. self.status = bug.pending
  215. self.reporter = bug.originator
  216. self.date = bug.date
  217. self.severity = bug.severity
  218. self.version = ', '.join(bug.found_versions)
  219. self.filed_date = bug.date
  220. self.modified_date = bug.log_modified
  221. self.info = bug.subject
  222. def __iter__(self):
  223. yield self.id
  224. yield self.tag
  225. yield self.package
  226. yield self.info
  227. yield self.status
  228. yield self.reporter
  229. yield self.date
  230. yield self.severity
  231. yield self.version
  232. yield self.filed_date
  233. yield self.modified_date
  234. class BugReport(object):
  235. def __init__(self, message):
  236. lines = message.split('\n')
  237. i = 0
  238. self.headers = []
  239. while i < len(lines):
  240. line = lines[i]
  241. i += 1
  242. if not line.strip():
  243. break
  244. self.headers.append(line)
  245. store = 0
  246. info = []
  247. while i < len(lines):
  248. line = lines[i]
  249. info.append(line)
  250. i += 1
  251. if store < 2 and not line.strip():
  252. store += 1
  253. continue
  254. if store == 2 and(line.startswith('-- ') or line.startswith('** ')):
  255. break
  256. store = 0
  257. self.original_info = '\n'.join(info[:-3])
  258. self.others = '\n'.join(lines[i - 1:])
  259. def get_others(self):
  260. return self.others
  261. def get_original_info(self):
  262. return self.original_info
  263. def get_subject(self):
  264. for header in self.headers:
  265. if 'Subject' in header:
  266. return header[len('Subject: '):]
  267. def set_subject(self, subject):
  268. for i in range(len(self.headers)):
  269. if 'Subject' in self.headers[i]:
  270. self.headers[i] = 'Subject: ' + subject
  271. break
  272. def wrap_bug_body(self, msg, width=79, break_long_words=False):
  273. """Wrap every line in the message"""
  274. # resulting body text
  275. body = ''
  276. for line in msg.splitlines():
  277. # wrap long lines, it returns a list of "sub-lines"
  278. tmp = textwrap.wrap(line, width=width,
  279. break_long_words=break_long_words)
  280. # need to special-case this else a join() on the list generator
  281. # would remove all the '[]' so no empty lines in the report
  282. if tmp == []:
  283. body += '\n'
  284. else:
  285. # join the "sub-lines" and add a \n at the end(if there is
  286. # only one item in the list, else there wouldn't be a \n)
  287. body += '\n'.join(tmp) + '\n'
  288. return body
  289. def create_message(self, info):
  290. message = """%s
  291. %s
  292. %s""" % ('\n'.join(self.headers), self.wrap_bug_body(info), self.others)
  293. return message
  294. # BTS GUI
  295. class BugPage(Gtk.EventBox, threading.Thread):
  296. def __init__(self, assistant, dialog, number, queryonly, bts, mirrors, http_proxy, timeout, archived):
  297. _assert_context(ui_context)
  298. threading.Thread.__init__(self)
  299. Gtk.EventBox.__init__(self)
  300. self.setDaemon(True)
  301. self.context = GLib.MainContext()
  302. self.dialog = dialog
  303. self.assistant = assistant
  304. self.application = self.assistant.application
  305. self.number = number
  306. self.queryonly = queryonly
  307. self.bts = bts
  308. self.mirrors = mirrors
  309. self.http_proxy = http_proxy
  310. self.timeout = timeout
  311. self.archived = archived
  312. self.bug_status = None
  313. vbox = Gtk.VBox(spacing=12)
  314. vbox.pack_start(Gtk.Label(label="Retrieving bug information."), False, True, 0)
  315. self.progress = Gtk.ProgressBar()
  316. self.progress.set_pulse_step(0.01)
  317. vbox.pack_start(self.progress, False, True, 0)
  318. self.add(vbox)
  319. def run(self):
  320. if not self.context.acquire():
  321. # should be impossible
  322. raise AssertionError('Could not acquire my own main-context')
  323. self.context.push_thread_default()
  324. # Start the progress bar
  325. GLib.timeout_add(10, self.pulse)
  326. info = debbugs.get_report(int(self.number), self.timeout,
  327. self.bts, mirrors=self.mirrors,
  328. http_proxy=self.http_proxy, archived=self.archived)
  329. if not info:
  330. self.application.run_once_in_main_thread(self.not_found)
  331. else:
  332. self.bug_status = info[0]
  333. self.application.run_once_in_main_thread(self.found, info)
  334. def drop_progressbar(self):
  335. _assert_context(ui_context)
  336. child = self.get_child()
  337. if child:
  338. self.remove(child)
  339. child.unparent()
  340. def pulse(self):
  341. _assert_context(ui_context)
  342. self.progress.pulse()
  343. return self.isAlive()
  344. def not_found(self):
  345. _assert_context(ui_context)
  346. self.drop_progressbar()
  347. self.add(Gtk.Label(label="The bug can't be fetched or it doesn't exist."))
  348. self.show_all()
  349. def found(self, info):
  350. _assert_context(ui_context)
  351. self.drop_progressbar()
  352. desc = info[0].subject
  353. bodies = info[1]
  354. vbox = Gtk.VBox(spacing=12)
  355. vbox.set_border_width(12)
  356. label = Gtk.Label(label='Description: ' + desc)
  357. label.set_line_wrap(True)
  358. label.set_justify(Gtk.Justification.FILL)
  359. vbox.pack_start(label, False, True, 0)
  360. views = Gtk.VBox()
  361. odd = False
  362. for body in bodies:
  363. view = Gtk.TextView()
  364. view.set_editable(False)
  365. view.get_buffer().set_text(body)
  366. if odd:
  367. view.set_state_flags(Gtk.StateFlags.PRELIGHT, False)
  368. views.pack_start(view, False, True, 0)
  369. odd = not odd
  370. scrolled = create_scrollable(views, True)
  371. vbox.pack_start(scrolled, True, True, 0)
  372. bbox = Gtk.HButtonBox()
  373. button = Gtk.Button(label="Open in browser")
  374. button.connect('clicked', self.on_open_browser)
  375. bbox.pack_start(button, True, True, 0)
  376. if not self.queryonly:
  377. button = Gtk.Button(label="Reply")
  378. button.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_EDIT, Gtk.IconSize.BUTTON))
  379. button.connect('clicked', self.on_reply)
  380. bbox.pack_start(button, True, True, 0)
  381. vbox.pack_start(bbox, False, True, 0)
  382. self.add(vbox)
  383. self.show_all()
  384. def on_open_browser(self, button):
  385. _assert_context(ui_context)
  386. launch_browser(debbugs.get_report_url(self.bts, int(self.number), self.archived))
  387. def on_reply(self, button):
  388. _assert_context(ui_context)
  389. # Return the bug number to reportbug
  390. self.application.set_next_value(self.bug_status)
  391. # Forward the assistant to the progress bar
  392. self.assistant.forward_page()
  393. # Though we're only a page, we are authorized to destroy our parent :)
  394. # This would be better handled connecting externally to self.reply_button
  395. self.dialog.destroy()
  396. class BugsDialog(Gtk.Dialog):
  397. def __init__(self, assistant, queryonly):
  398. _assert_context(ui_context)
  399. Gtk.Dialog.__init__(self, "Reportbug: bug information", assistant,
  400. Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
  401. (Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE))
  402. self.assistant = assistant
  403. self.queryonly = queryonly
  404. self.application = assistant.application
  405. self.notebook = Gtk.Notebook()
  406. self.vbox.pack_start(self.notebook, True, True, 0)
  407. self.connect('response', self.on_response)
  408. self.set_default_size(600, 600)
  409. def on_response(self, *args):
  410. self.destroy()
  411. def show_bug(self, number, *args):
  412. page = BugPage(self.assistant, self, number, self.queryonly, *args)
  413. self.notebook.append_page(page, Gtk.Label(label=number))
  414. page.start()
  415. # Application
  416. class ReportbugApplication(threading.Thread):
  417. def __init__(self):
  418. _assert_context(reportbug_context)
  419. threading.Thread.__init__(self)
  420. self.setDaemon(True)
  421. self.queue = Queue()
  422. self.next_value = None
  423. def run(self):
  424. if not ui_context.acquire():
  425. # should be impossible
  426. raise AssertionError('Could not acquire UI context')
  427. ui_context.push_thread_default()
  428. Gtk.main()
  429. def get_last_value(self):
  430. _assert_context(reportbug_context)
  431. return self.queue.get()
  432. def put_next_value(self):
  433. _assert_context(ui_context)
  434. self.queue.put(self.next_value)
  435. self.next_value = None
  436. def set_next_value(self, value):
  437. _assert_context(ui_context)
  438. self.next_value = value
  439. def run_once_in_main_thread(self, func, *args, **kwargs):
  440. # OK to call from any thread
  441. def callback():
  442. _assert_context(ui_context)
  443. func(*args, **kwargs)
  444. return False
  445. GLib.idle_add(callback)
  446. def call_in_main_thread(self, func, *args, **kwargs):
  447. # OK to call from any thread
  448. def callback():
  449. _assert_context(ui_context)
  450. try:
  451. ret = func(*args, **kwargs)
  452. except BaseException as e:
  453. self.set_next_value(e)
  454. else:
  455. self.set_next_value(ret)
  456. self.put_next_value()
  457. return False
  458. GLib.idle_add(callback)
  459. ret = self.get_last_value()
  460. if isinstance(ret, BaseException):
  461. raise ret
  462. else:
  463. return ret
  464. # Connection with reportbug
  465. # Syncronize "pipe" with reportbug
  466. class SyncReturn(RuntimeError):
  467. def __init__(self, result):
  468. _assert_context(reportbug_context)
  469. RuntimeError.__init__(self, result)
  470. self.result = result
  471. class ReportbugConnector(object):
  472. def execute_operation(self, *args, **kwargs):
  473. _assert_context(ui_context)
  474. pass
  475. # Executed in sync with reportbug. raise SyncResult(value) to directly return to reportbug
  476. # Returns args and kwargs to pass to execute_operation
  477. def sync_pre_operation(cls, *args, **kwargs):
  478. _assert_context(reportbug_context)
  479. return args, kwargs
  480. # Assistant
  481. class Page(ReportbugConnector):
  482. next_page_num = 0
  483. page_type = Gtk.AssistantPageType.CONTENT
  484. default_complete = False
  485. side_image = DEBIAN_LOGO
  486. WARNING_COLOR = Gdk.color_parse("#fff8ae")
  487. def __init__(self, assistant):
  488. _assert_context(ui_context)
  489. self.assistant = assistant
  490. self.application = assistant.application
  491. self.widget = self.create_widget()
  492. self.widget.page = self
  493. self.widget.set_border_width(6)
  494. self.widget.show_all()
  495. self.page_num = Page.next_page_num
  496. def execute_operation(self, *args, **kwargs):
  497. _assert_context(ui_context)
  498. self.switch_in()
  499. self.connect_signals()
  500. self.empty_ok = kwargs.pop('empty_ok', False)
  501. self.presubj = kwargs.pop('presubj', False)
  502. self.execute(*args, **kwargs)
  503. self.assistant.show()
  504. self.setup_focus()
  505. def connect_signals(self):
  506. _assert_context(ui_context)
  507. def set_page_complete(self, complete):
  508. _assert_context(ui_context)
  509. self.assistant.set_page_complete(self.widget, complete)
  510. def set_page_type(self, type):
  511. _assert_context(ui_context)
  512. self.assistant.set_page_type(self.widget, type)
  513. def set_page_title(self, title):
  514. _assert_context(ui_context)
  515. if title:
  516. self.assistant.set_page_title(self.widget, title)
  517. # The user will see this as next page
  518. def switch_in(self):
  519. _assert_context(ui_context)
  520. Page.next_page_num += 1
  521. self.assistant.insert_page(self.widget, self.page_num)
  522. self.set_page_complete(self.default_complete)
  523. self.set_page_type(self.page_type)
  524. self.assistant.set_page_side_image(self.widget, GdkPixbuf.Pixbuf.new_from_file(self.side_image))
  525. self.assistant.set_next_page(self)
  526. self.set_page_title("Reportbug")
  527. # Setup keyboard focus in the page
  528. def setup_focus(self):
  529. _assert_context(ui_context)
  530. self.widget.grab_focus()
  531. # Forward page when a widget is activated(e.g. GtkEntry) only if page is complete
  532. def activate_forward(self, *args):
  533. _assert_context(ui_context)
  534. if self.assistant.get_page_complete(self.widget):
  535. self.assistant.forward_page()
  536. # The user forwarded the assistant to see the next page
  537. def switch_out(self):
  538. _assert_context(ui_context)
  539. def is_valid(self, value):
  540. _assert_context(ui_context)
  541. if self.empty_ok:
  542. return True
  543. else:
  544. return bool(value)
  545. def validate(self, *args, **kwargs):
  546. _assert_context(ui_context)
  547. value = self.get_value()
  548. if self.is_valid(value):
  549. self.application.set_next_value(value)
  550. self.set_page_complete(True)
  551. else:
  552. self.set_page_complete(False)
  553. class IntroPage(Page):
  554. page_type = Gtk.AssistantPageType.INTRO
  555. default_complete = True
  556. def create_widget(self):
  557. _assert_context(ui_context)
  558. vbox = Gtk.VBox(spacing=24)
  559. label = Gtk.Label(label="""
  560. <b>Reportbug</b> is a tool designed to make the reporting of bugs in Debian and derived distributions relatively painless.
  561. This wizard will guide you through the bug reporting process step by step.
  562. <b>Note:</b> bug reports are publicly archived(including the email address of the submitter).""")
  563. label.set_use_markup(True)
  564. label.set_line_wrap(True)
  565. label.set_justify(Gtk.Justification.FILL)
  566. vbox.pack_start(label, False, True, 0)
  567. link = Gtk.LinkButton.new_with_label("http://alioth.debian.org/projects/reportbug",
  568. "Homepage of reportbug project")
  569. vbox.pack_start(link, False, True, 0)
  570. return vbox
  571. class GetStringPage(Page):
  572. def setup_focus(self):
  573. _assert_context(ui_context)
  574. self.entry.grab_focus()
  575. def create_widget(self):
  576. _assert_context(ui_context)
  577. vbox = Gtk.VBox(spacing=12)
  578. self.label = Gtk.Label()
  579. self.label.set_line_wrap(True)
  580. self.label.set_justify(Gtk.Justification.FILL)
  581. self.label.set_selectable(True)
  582. self.label.set_property("can-focus", False)
  583. self.entry = Gtk.Entry()
  584. vbox.pack_start(self.label, False, True, 0)
  585. vbox.pack_start(self.entry, False, True, 0)
  586. return vbox
  587. def connect_signals(self):
  588. _assert_context(ui_context)
  589. self.entry.connect('changed', self.validate)
  590. self.entry.connect('activate', self.activate_forward)
  591. def get_value(self):
  592. _assert_context(ui_context)
  593. return self.entry.get_text()
  594. def execute(self, prompt, options=None, force_prompt=False, default=''):
  595. _assert_context(ui_context)
  596. # Hackish: remove the text needed for textual UIs...
  597. GLib.idle_add(self.label.set_text, prompt.replace('(enter Ctrl+c to exit reportbug without reporting a bug)', ''))
  598. self.entry.set_text(default)
  599. if options:
  600. options.sort()
  601. completion = Gtk.EntryCompletion()
  602. model = Gtk.ListStore(str)
  603. for option in options:
  604. model.append([option])
  605. completion.set_model(model)
  606. completion.set_inline_selection(True)
  607. completion.set_text_column(0)
  608. self.entry.set_completion(completion)
  609. else:
  610. self.completion = None
  611. self.validate()
  612. class GetPasswordPage(GetStringPage):
  613. def create_widget(self):
  614. _assert_context(ui_context)
  615. widget = GetStringPage.create_widget(self)
  616. self.entry.set_visibility(False)
  617. return widget
  618. class GetMultilinePage(Page):
  619. def setup_focus(self):
  620. _assert_context(ui_context)
  621. self.view.grab_focus()
  622. def create_widget(self):
  623. _assert_context(ui_context)
  624. vbox = Gtk.VBox(spacing=12)
  625. self.label = Gtk.Label()
  626. self.label.set_line_wrap(True)
  627. self.label.set_justify(Gtk.Justification.FILL)
  628. self.label.set_selectable(True)
  629. self.label.set_property("can-focus", False)
  630. vbox.pack_start(self.label, False, True, 0)
  631. self.view = Gtk.TextView()
  632. self.buffer = self.view.get_buffer()
  633. scrolled = create_scrollable(self.view)
  634. vbox.pack_start(scrolled, True, True, 0)
  635. return vbox
  636. def connect_signals(self):
  637. _assert_context(ui_context)
  638. self.buffer.connect('changed', self.validate)
  639. def get_value(self):
  640. _assert_context(ui_context)
  641. text = self.buffer.get_text(self.buffer.get_start_iter(), self.buffer.get_end_iter())
  642. lines = text.split('\n')
  643. # Remove the trailing empty line at the end
  644. if len(lines) > 0 and not lines[-1].strip():
  645. del lines[-1]
  646. return text.split('\n')
  647. def execute(self, prompt):
  648. _assert_context(ui_context)
  649. self.empty_ok = True
  650. # The result must be iterable for reportbug even if it's empty and not modified
  651. GLib.idle_add(self.label.set_text, prompt)
  652. self.buffer.set_text("")
  653. self.buffer.emit('changed')
  654. class TreePage(Page):
  655. value_column = None
  656. def __init__(self, *args, **kwargs):
  657. _assert_context(ui_context)
  658. Page.__init__(self, *args, **kwargs)
  659. self.selection = self.view.get_selection()
  660. def setup_focus(self):
  661. _assert_context(ui_context)
  662. self.view.grab_focus()
  663. def connect_signals(self):
  664. _assert_context(ui_context)
  665. self.selection.connect('changed', self.validate)
  666. def get_value(self):
  667. _assert_context(ui_context)
  668. model, paths = self.selection.get_selected_rows()
  669. multiple = self.selection.get_mode() == Gtk.SelectionMode.MULTIPLE
  670. result = []
  671. for path in paths:
  672. value = model.get_value(model.get_iter(path), self.value_column)
  673. if value is not None:
  674. result.append(markup_free(value))
  675. if result and not multiple:
  676. return result[0]
  677. return result
  678. class GetListPage(TreePage):
  679. value_column = 0
  680. def create_widget(self):
  681. _assert_context(ui_context)
  682. vbox = Gtk.VBox(spacing=12)
  683. self.label = Gtk.Label()
  684. self.label.set_line_wrap(True)
  685. self.label.set_justify(Gtk.Justification.FILL)
  686. vbox.pack_start(self.label, False, True, 0)
  687. hbox = Gtk.HBox(spacing=6)
  688. self.view = Gtk.TreeView()
  689. self.view.set_rules_hint(True)
  690. self.view.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
  691. scrolled = create_scrollable(self.view)
  692. hbox.pack_start(scrolled, True, True, 0)
  693. bbox = Gtk.VButtonBox()
  694. bbox.set_spacing(6)
  695. bbox.set_layout(Gtk.ButtonBoxStyle.START)
  696. button = Gtk.Button(stock=Gtk.STOCK_ADD)
  697. button.connect('clicked', self.on_add)
  698. bbox.pack_start(button, False, True, 0)
  699. button = Gtk.Button(stock=Gtk.STOCK_REMOVE)
  700. button.connect('clicked', self.on_remove)
  701. bbox.pack_start(button, False, True, 0)
  702. hbox.pack_start(bbox, False, True, 0)
  703. vbox.pack_start(hbox, True, True, 0)
  704. return vbox
  705. def get_value(self):
  706. _assert_context(ui_context)
  707. values = []
  708. for row in self.model:
  709. values.append(row[self.value_column])
  710. return values
  711. def on_add(self, button):
  712. _assert_context(ui_context)
  713. dialog = InputStringDialog("Add a new item to the list")
  714. dialog.show_all()
  715. dialog.connect('response', self.on_add_dialog_response)
  716. def on_add_dialog_response(self, dialog, res):
  717. _assert_context(ui_context)
  718. if res == Gtk.ResponseType.ACCEPT:
  719. self.model.append([dialog.get_value()])
  720. dialog.destroy()
  721. def on_remove(self, button):
  722. _assert_context(ui_context)
  723. model, paths = self.selection.get_selected_rows()
  724. # We need to transform them to iters, since paths change when removing rows
  725. iters = []
  726. for path in paths:
  727. iters.append(self.model.get_iter(path))
  728. for iter in iters:
  729. self.model.remove(iter)
  730. def execute(self, prompt):
  731. _assert_context(ui_context)
  732. self.empty_ok = True
  733. GLib.idle_add(self.label.set_text, prompt)
  734. self.model = Gtk.ListStore(str)
  735. self.model.connect('row-changed', self.validate)
  736. self.view.set_model(self.model)
  737. self.selection.set_mode(Gtk.SelectionMode.MULTIPLE)
  738. self.view.append_column(Gtk.TreeViewColumn('Item', Gtk.CellRendererText(), text=0))
  739. class WrapRendererText(Gtk.CellRendererText):
  740. def do_render(self, cr, widget, background_area, cell_area, flags):
  741. _assert_context(ui_context)
  742. self.set_property('wrap-width', cell_area.width)
  743. Gtk.CellRendererText.do_render(self, cr, widget, background_area, cell_area, flags)
  744. GObject.type_register(WrapRendererText)
  745. class MenuPage(TreePage):
  746. value_column = 0
  747. def create_widget(self):
  748. _assert_context(ui_context)
  749. vbox = Gtk.VBox(spacing=12)
  750. self.label = Gtk.Label()
  751. self.label.set_line_wrap(True)
  752. self.label.set_justify(Gtk.Justification.FILL)
  753. vbox.pack_start(self.label, False, True, 0)
  754. self.view = Gtk.TreeView()
  755. self.view.set_rules_hint(True)
  756. scrolled = create_scrollable(self.view)
  757. scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.ALWAYS)
  758. vbox.pack_start(scrolled, True, True, 0)
  759. vbox.show_all()
  760. return vbox
  761. def connect_signals(self):
  762. _assert_context(ui_context)
  763. TreePage.connect_signals(self)
  764. self.view.connect('row-activated', self.activate_forward)
  765. def execute(self, par, options, prompt, default=None, any_ok=False,
  766. order=None, extras=None, multiple=False):
  767. _assert_context(ui_context)
  768. GLib.idle_add(self.label.set_text, par)
  769. self.model = Gtk.ListStore(str, str)
  770. self.view.set_model(self.model)
  771. if multiple:
  772. self.selection.set_mode(Gtk.SelectionMode.MULTIPLE)
  773. self.view.append_column(Gtk.TreeViewColumn('Option', Gtk.CellRendererText(), markup=0))
  774. rend = WrapRendererText()
  775. rend.set_property('wrap-mode', Pango.WrapMode.WORD)
  776. rend.set_property('wrap-width', 300)
  777. self.view.append_column(Gtk.TreeViewColumn('Description', rend, text=1))
  778. default_iter = None
  779. # here below, 'text' is the value of the description of the item, but
  780. # writen all on a single-line, it will be wrapped by the list settings
  781. if isinstance(options, dict):
  782. if order:
  783. for option in order:
  784. if option in options:
  785. text = ' '.join(options[option].split())
  786. iter = self.model.append((highlight(option), text))
  787. if option == default:
  788. default_iter = iter
  789. for option, desc in options.items():
  790. if not order or option not in order:
  791. text = ' '.join(desc.split())
  792. iter = self.model.append((highlight(option), text))
  793. if option == default:
  794. default_iter = iter
  795. else:
  796. for row in options:
  797. text = ' '.join(row[1].split())
  798. iter = self.model.append((highlight(row[0]), text))
  799. if row[0] == default:
  800. default_iter = iter
  801. if default_iter:
  802. self.selection.select_iter(default_iter)
  803. class HandleBTSQueryPage(TreePage):
  804. default_complete = True
  805. value_column = 0
  806. def sync_pre_operation(self, package, bts, timeout, mirrors=None, http_proxy="", queryonly=False, screen=None,
  807. archived='no', source=False, title=None,
  808. version=None, buglist=None, mbox_reader_cmd=None, latest_first=False):
  809. _assert_context(reportbug_context)
  810. self.bts = bts
  811. self.mirrors = mirrors
  812. self.http_proxy = http_proxy
  813. self.timeout = timeout
  814. self.archived = archived
  815. self.queryonly = queryonly
  816. if queryonly:
  817. self.page_type = Gtk.AssistantPageType.CONFIRM
  818. sysinfo = debbugs.SYSTEMS[bts]
  819. root = sysinfo.get('btsroot')
  820. if not root:
  821. # do we need to make a dialog for this?
  822. return
  823. if isinstance(package, str):
  824. pkgname = package
  825. if source:
  826. pkgname += '(source)'
  827. progress_label = 'Querying %s bug tracking system for reports on %s' % (debbugs.SYSTEMS[bts]['name'], pkgname)
  828. else:
  829. progress_label = 'Querying %s bug tracking system for reports %s' % (debbugs.SYSTEMS[bts]['name'], ' '.join([str(x) for x in package]))
  830. self.application.run_once_in_main_thread(self.assistant.set_progress_label, progress_label)
  831. try:
  832. (count, sectitle, hierarchy) = debbugs.get_reports(
  833. package, timeout, bts, mirrors=mirrors, version=version,
  834. http_proxy=http_proxy, archived=archived, source=source)
  835. except:
  836. error_dialog("Unable to connect to %s BTS." % sysinfo['name'])
  837. raise NoBugs
  838. try:
  839. if not count:
  840. if hierarchy is None:
  841. raise NoPackage
  842. else:
  843. raise NoBugs
  844. else:
  845. if count > 1:
  846. sectitle = '%d bug reports found' % (count,)
  847. else:
  848. sectitle = 'One bug report found'
  849. report = []
  850. for category, bugs in hierarchy:
  851. buglist = []
  852. for bug in bugs:
  853. buglist.append(bug)
  854. # XXX: this needs to be fixed in debianbts; Bugreport are
  855. # not sortable(on bug_num) - see #639458
  856. sorted(buglist, reverse=latest_first)
  857. report.append((category, list(map(Bug, buglist))))
  858. return(report, sectitle), {}
  859. except NoPackage:
  860. error_dialog('No record of this package found.')
  861. raise NoPackage
  862. raise SyncReturn(None)
  863. def setup_focus(self):
  864. _assert_context(ui_context)
  865. self.entry.grab_focus()
  866. def create_widget(self):
  867. _assert_context(ui_context)
  868. vbox = Gtk.VBox(spacing=6)
  869. self.label = Gtk.Label(label="List of bugs. Select a bug to retrieve and submit more information.")
  870. vbox.pack_start(self.label, False, True, 6)
  871. hbox = Gtk.HBox(spacing=6)
  872. label = Gtk.Label(label="Filter:")
  873. hbox.pack_start(label, False, True, 0)
  874. self.entry = Gtk.Entry()
  875. hbox.pack_start(self.entry, True, True, 0)
  876. button = Gtk.Button()
  877. button.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_CLEAR, Gtk.IconSize.MENU))
  878. button.set_relief(Gtk.ReliefStyle.NONE)
  879. button.connect('clicked', self.on_filter_clear)
  880. hbox.pack_start(button, False, True, 0)
  881. vbox.pack_start(hbox, False, True, 0)
  882. self.view = Gtk.TreeView()
  883. self.view.set_rules_hint(True)
  884. scrolled = create_scrollable(self.view)
  885. self.columns = ['ID', 'Tag', 'Package', 'Description', 'Status', 'Submitter', 'Date', 'Severity', 'Version',
  886. 'Filed date', 'Modified date']
  887. for col in zip(self.columns, list(range(len(self.columns)))):
  888. column = Gtk.TreeViewColumn(col[0], Gtk.CellRendererText(), text=col[1])
  889. column.set_reorderable(True)
  890. self.view.append_column(column)
  891. vbox.pack_start(scrolled, True, True, 0)
  892. button = Gtk.Button(label="Retrieve and submit bug information")
  893. button.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_INFO, Gtk.IconSize.BUTTON))
  894. button.connect('clicked', self.on_retrieve_info)
  895. vbox.pack_start(button, False, True, 0)
  896. return vbox
  897. def connect_signals(self):
  898. _assert_context(ui_context)
  899. TreePage.connect_signals(self)
  900. self.view.connect('row-activated', self.on_retrieve_info)
  901. self.entry.connect('changed', self.on_filter_changed)
  902. def on_filter_clear(self, button):
  903. _assert_context(ui_context)
  904. self.entry.set_text("")
  905. def on_filter_changed(self, entry):
  906. _assert_context(ui_context)
  907. self.model.filter_text = entry.get_text().lower()
  908. self.filter.refilter()
  909. def on_retrieve_info(self, *args):
  910. _assert_context(ui_context)
  911. bug_ids = TreePage.get_value(self)
  912. if not bug_ids:
  913. info_dialog("Please select one ore more bugs")
  914. return
  915. dialog = BugsDialog(self.assistant, self.queryonly)
  916. for id in bug_ids:
  917. dialog.show_bug(id, self.bts, self.mirrors, self.http_proxy, self.timeout, self.archived)
  918. dialog.show_all()
  919. def is_valid(self, value):
  920. _assert_context(ui_context)
  921. return True
  922. def get_value(self):
  923. _assert_context(ui_context)
  924. # The value returned to reportbug doesn't depend by a selection, but by the dialog of a bug
  925. return None
  926. def match_filter(self, iter):
  927. _assert_context(ui_context)
  928. # Flatten the columns into a single string
  929. text = ""
  930. for col in range(len(self.columns)):
  931. value = self.model.get_value(iter, col)
  932. if value:
  933. text += self.model.get_value(iter, col) + " "
  934. text = text.lower()
  935. # Tokens shouldn't be adjacent by default
  936. for token in self.model.filter_text.split(' '):
  937. if token in text:
  938. return True
  939. return False
  940. def filter_visible_func(self, model, iter, user_data=None):
  941. _assert_context(ui_context)
  942. matches = self.match_filter(iter)
  943. if not self.model.iter_parent(iter) and not matches:
  944. # If no children are visible, hide it
  945. it = model.iter_children(iter)
  946. while it:
  947. if self.match_filter(it):
  948. return True
  949. it = model.iter_next(it)
  950. return False
  951. return matches
  952. def execute(self, buglist, sectitle):
  953. _assert_context(ui_context)
  954. GLib.idle_add(self.label.set_text, "%s. Double-click a bug to retrieve and submit more information." % sectitle)
  955. self.model = Gtk.TreeStore(*([str] * len(self.columns)))
  956. for category in buglist:
  957. row = [None] * len(self.columns)
  958. row[3] = category[0]
  959. iter = self.model.append(None, row)
  960. for bug in category[1]:
  961. self.model.append(iter, list(map(str, bug)))
  962. self.selection.set_mode(Gtk.SelectionMode.MULTIPLE)
  963. self.model.filter_text = ""
  964. self.filter = self.model.filter_new()
  965. self.filter.set_visible_func(self.filter_visible_func)
  966. self.view.set_model(self.filter)
  967. class ShowReportPage(Page):
  968. default_complete = True
  969. def create_widget(self):
  970. _assert_context(ui_context)
  971. self.page = BugPage(self.assistant, None, None, None, None, None, None, None, None)
  972. return self.page
  973. def get_value(self):
  974. _assert_context(ui_context)
  975. return None
  976. def is_valid(self, value):
  977. _assert_context(ui_context)
  978. return True
  979. def sync_pre_operation(self, *args, **kwargs):
  980. _assert_context(reportbug_context)
  981. if kwargs.get('queryonly'):
  982. self.page_type = Gtk.AssistantPageType.CONFIRM
  983. return args, kwargs
  984. def execute(self, number, system, mirrors, http_proxy, timeout, queryonly=False, title='', archived='no', mbox_reader_cmd=None):
  985. _assert_context(ui_context)
  986. self.page.number = number
  987. self.page.bts = system
  988. self.page.mirrors = mirrors
  989. self.page.http_proxy = http_proxy
  990. self.page.timeout = timeout
  991. self.page.queryonly = queryonly
  992. self.page.archived = archived
  993. self.page.start()
  994. self.validate()
  995. class DisplayReportPage(Page):
  996. default_complete = True
  997. def create_widget(self):
  998. _assert_context(ui_context)
  999. self.view = Gtk.TextView()
  1000. self.view.set_editable(False)
  1001. scrolled = create_scrollable(self.view)
  1002. return scrolled
  1003. def execute(self, message, *args):
  1004. _assert_context(ui_context)
  1005. # 'use' args only if it's passed
  1006. if args:
  1007. message = message % args
  1008. self.view.get_buffer().set_text(message)
  1009. class LongMessagePage(Page):
  1010. default_complete = True
  1011. def create_widget(self):
  1012. _assert_context(ui_context)
  1013. self.label = Gtk.Label()
  1014. self.label.set_line_wrap(True)
  1015. self.label.set_justify(Gtk.Justification.FILL)
  1016. self.label.set_selectable(True)
  1017. self.label.set_property("can-focus", False)
  1018. eb = Gtk.EventBox()
  1019. eb.add(self.label)
  1020. return eb
  1021. def execute(self, message, *args):
  1022. _assert_context(ui_context)
  1023. message = message % args
  1024. # make it all on one line, it will be wrapped at display-time
  1025. message = ' '.join(message.split())
  1026. GLib.idle_add(self.label.set_text, message)
  1027. # Reportbug should use final_message, so emulate it
  1028. if('999999' in message):
  1029. self.set_page_type(Gtk.AssistantPageType.CONFIRM)
  1030. self.set_page_title("Thanks for your report")
  1031. class FinalMessagePage(LongMessagePage):
  1032. page_type = Gtk.AssistantPageType.CONFIRM
  1033. default_complete = True
  1034. def execute(self, *args, **kwargs):
  1035. _assert_context(ui_context)
  1036. LongMessagePage.execute(self, *args, **kwargs)
  1037. self.set_page_title("Thanks for your report")
  1038. class EditorPage(Page):
  1039. def create_widget(self):
  1040. _assert_context(ui_context)
  1041. vbox = Gtk.VBox(spacing=6)
  1042. hbox = Gtk.HBox(spacing=12)
  1043. hbox.pack_start(Gtk.Label(label="Subject: "), False, True, 0)
  1044. self.subject = Gtk.Entry()
  1045. hbox.pack_start(self.subject, True, True, 0)
  1046. vbox.pack_start(hbox, False, True, 0)
  1047. self.view = Gtk.TextView()
  1048. self.view.modify_font(Pango.FontDescription("Monospace"))
  1049. self.view.set_wrap_mode(Gtk.WrapMode.WORD)
  1050. # We have to do the import in the UI thread, because it loads a
  1051. # SQLite database at import time, and the Python SQLite bindings
  1052. # don't allow transferring a SQLite handle between threads.
  1053. global gtkspellcheck
  1054. if gtkspellcheck is None:
  1055. try:
  1056. import gtkspellcheck
  1057. except:
  1058. gtkspellcheck = NotImplemented
  1059. if gtkspellcheck is not NotImplemented:
  1060. gtkspellcheck.SpellChecker(self.view)
  1061. self.info_buffer = self.view.get_buffer()
  1062. scrolled = create_scrollable(self.view)
  1063. vbox.pack_start(scrolled, True, True, 0)
  1064. expander = Gtk.Expander.new_with_mnemonic("Other system information")
  1065. view = Gtk.TextView()
  1066. view.set_editable(False)
  1067. self.others_buffer = view.get_buffer()
  1068. scrolled = create_scrollable(view)
  1069. expander.add(scrolled)
  1070. vbox.pack_start(expander, False, True, 0)
  1071. if gtkspellcheck is NotImplemented:
  1072. box = Gtk.EventBox()
  1073. label = Gtk.Label(label="Please install <b>python3-gtkspellcheck</b> to enable spell checking")
  1074. label.set_use_markup(True)
  1075. label.set_line_wrap(True)
  1076. label.set_selectable(True)
  1077. label.set_property("can-focus", False)
  1078. box.add(label)
  1079. box.modify_bg(Gtk.StateType.NORMAL, self.WARNING_COLOR)
  1080. box.connect('button-press-event', lambda *args: box.destroy())
  1081. vbox.pack_start(box, False, True, 0)
  1082. return vbox
  1083. def switch_out(self):
  1084. global report_message
  1085. _assert_context(ui_context)
  1086. report_message = self.get_value()[0]
  1087. f = open(self.filename, "w")
  1088. f.write(report_message)
  1089. f.close()
  1090. def connect_signals(self):
  1091. _assert_context(ui_context)
  1092. self.info_buffer.connect('changed', self.validate)
  1093. self.subject.connect('changed', self.validate)
  1094. def get_value(self):
  1095. _assert_context(ui_context)
  1096. info = self.info_buffer.get_text(self.info_buffer.get_start_iter(),
  1097. self.info_buffer.get_end_iter(),
  1098. True)
  1099. if not info.strip():
  1100. return None
  1101. subject = self.subject.get_text().strip()
  1102. if not subject.strip():
  1103. return None
  1104. self.report.set_subject(subject)
  1105. message = self.report.create_message(info)
  1106. return(message, message != self.message)
  1107. def handle_first_info(self):
  1108. _assert_context(ui_context)
  1109. self.focus_in_id = self.view.connect('focus-in-event', self.on_view_focus_in_event)
  1110. def on_view_focus_in_event(self, view, *args):
  1111. _assert_context(ui_context)
  1112. # Empty the buffer only the first time
  1113. self.info_buffer.set_text("")
  1114. view.disconnect(self.focus_in_id)
  1115. def execute(self, message, filename, editor, charset='utf-8'):
  1116. _assert_context(ui_context)
  1117. self.message = message
  1118. self.report = BugReport(message)
  1119. self.filename = filename
  1120. self.charset = charset
  1121. self.subject.set_text(self.report.get_subject())
  1122. self.others_buffer.set_text(self.report.get_others())
  1123. info = self.report.get_original_info()
  1124. if info.strip() == "*** Please type your report below this line ***":
  1125. info = "Please type your report here.\nThe text will be wrapped to be max 79 chars long per line."
  1126. self.handle_first_info()
  1127. self.info_buffer.set_text(info)
  1128. class SelectOptionsPage(Page):
  1129. default_complete = False
  1130. def create_widget(self):
  1131. _assert_context(ui_context)
  1132. self.label = Gtk.Label()
  1133. self.label.set_line_wrap(True)
  1134. self.label.set_justify(Gtk.Justification.FILL)
  1135. self.vbox = Gtk.VBox(spacing=6)
  1136. self.vbox.pack_start(self.label, False, True, 6)
  1137. self.default = None
  1138. return self.vbox
  1139. def on_clicked(self, button, menuopt):
  1140. _assert_context(ui_context)
  1141. self.application.set_next_value(menuopt)
  1142. self.assistant.forward_page()
  1143. def on_display_clicked(self, button):
  1144. global report_message
  1145. _assert_context(ui_context)
  1146. ReportViewerDialog(report_message)
  1147. def setup_focus(self):
  1148. _assert_context(ui_context)
  1149. if self.default:
  1150. self.default.props.can_default = True
  1151. self.default.props.has_default = True
  1152. self.default.grab_default()
  1153. self.default.grab_focus()
  1154. def execute(self, prompt, menuopts, options):
  1155. _assert_context(ui_context)
  1156. # remove text UI indication
  1157. prompt = prompt.replace('(e to edit)', '')
  1158. GLib.idle_add(self.label.set_text, prompt)
  1159. buttons = []
  1160. for menuopt in menuopts:
  1161. desc = options[menuopt.lower()]
  1162. # do we really need to launch an external editor?
  1163. if 'Change editor' in desc:
  1164. continue
  1165. # this will be handled using the text view below
  1166. if 'Pipe the message through the pager' in desc:
  1167. continue
  1168. # stdout is a textview for us
  1169. if 'Print message to stdout' in desc:
  1170. button = Gtk.Button(label="Display message in a text view")
  1171. button.connect('clicked', self.on_display_clicked)
  1172. buttons.append(button)
  1173. else:
  1174. button = Gtk.Button()
  1175. label = Gtk.Label(label=options[menuopt.lower()])
  1176. button.add(label)
  1177. button.connect('clicked', self.on_clicked, menuopt.lower())
  1178. if menuopt.isupper():
  1179. label.set_markup("<b>%s</b>" % label.get_text())
  1180. self.default = button
  1181. buttons.insert(0, Gtk.HSeparator())
  1182. buttons.insert(0, button)
  1183. else:
  1184. buttons.append(button)
  1185. for button in buttons:
  1186. self.vbox.pack_start(button, False, True, 0)
  1187. self.vbox.show_all()
  1188. class SystemPage(Page):
  1189. default_complete = False
  1190. def create_widget(self):
  1191. _assert_context(ui_context)
  1192. hbox = Gtk.HBox()
  1193. self.terminal = Vte.Terminal()
  1194. self.terminal.set_cursor_blinks(True)
  1195. self.terminal.set_emulation("xterm")
  1196. self.terminal.connect('child-exited', self.on_child_exited)
  1197. hbox.pack_start(self.terminal, True, True, 0)
  1198. scrollbar = Gtk.VScrollbar()
  1199. scrollbar.set_adjustment(self.terminal.get_adjustment())
  1200. hbox.pack_start(scrollbar, True, True, 0)
  1201. return hbox
  1202. def on_child_exited(self, terminal):
  1203. _assert_context(ui_context)
  1204. self.application.set_next_value(None)
  1205. self.assistant.forward_page()
  1206. def execute(self, cmdline):
  1207. _assert_context(ui_context)
  1208. self.terminal.fork_command('/bin/bash', ['/bin/bash', '-c', cmdline])
  1209. class ProgressPage(Page):
  1210. page_type = Gtk.AssistantPageType.PROGRESS
  1211. def pulse(self):
  1212. _assert_context(ui_context)
  1213. self.progress.pulse()
  1214. return True
  1215. def create_widget(self):
  1216. _assert_context(ui_context)
  1217. vbox = Gtk.VBox(spacing=6)
  1218. self.label = Gtk.Label()
  1219. self.label.set_line_wrap(True)
  1220. self.label.set_justify(Gtk.Justification.FILL)
  1221. self.progress = Gtk.ProgressBar()
  1222. self.progress.set_pulse_step(0.01)
  1223. vbox.pack_start(self.label, False, True, 0)
  1224. vbox.pack_start(self.progress, False, True, 0)
  1225. GLib.timeout_add(10, self.pulse)
  1226. return vbox
  1227. def set_label(self, text):
  1228. _assert_context(ui_context)
  1229. GLib.idle_add(self.label.set_text, text)
  1230. def reset_label(self):
  1231. _assert_context(ui_context)
  1232. self.set_label("This operation may take a while")
  1233. class ReportbugAssistant(Gtk.Assistant):
  1234. def __init__(self, application):
  1235. _assert_context(ui_context)
  1236. Gtk.Assistant.__init__(self)
  1237. self.application = application
  1238. self.set_title('Reportbug')
  1239. self.hack_buttons()
  1240. self.showing_page = None
  1241. self.requested_page = None
  1242. self.progress_page = None
  1243. self.set_default_size(600, 400)
  1244. self.set_forward_page_func(self.forward)
  1245. self.connect_signals()
  1246. self.setup_pages()
  1247. def _hack_buttons(self, widget):
  1248. _assert_context(ui_context)
  1249. # This is a real hack for two reasons:
  1250. # 1. There's no other way to access action area but inspecting the assistant and searching for the back button
  1251. # 2. Hide back button on show, because it can be shown-hidden by the assistant depending on the page
  1252. if isinstance(widget, Gtk.Button):
  1253. if widget.get_label() == 'gtk-go-back':
  1254. widget.connect('show', self.on_back_show)
  1255. return
  1256. if widget.get_label() == 'gtk-apply':
  1257. widget.connect('show', self.on_back_show)
  1258. return
  1259. if widget.get_label() == 'gtk-cancel':
  1260. image = Gtk.Image.new_from_stock(Gtk.STOCK_QUIT,
  1261. Gtk.IconSize.BUTTON)
  1262. widget.set_label("_Quit")
  1263. widget.set_image(image)
  1264. return
  1265. if widget.get_label() == 'gtk-go-forward':
  1266. image = Gtk.Image.new_from_stock(Gtk.STOCK_GO_FORWARD, Gtk.IconSize.BUTTON)
  1267. widget.set_label("_Continue")
  1268. widget.set_image(image)
  1269. return
  1270. if isinstance(widget, Gtk.Container):
  1271. widget.forall(self._hack_buttons)
  1272. def hack_buttons(self):
  1273. _assert_context(ui_context)
  1274. self._hack_buttons(self)
  1275. def connect_signals(self):
  1276. _assert_context(ui_context)
  1277. self.connect('cancel', self.confirm_exit)
  1278. self.connect('prepare', self.on_prepare)
  1279. self.connect('delete-event', self.close)
  1280. self.connect('apply', self.close)
  1281. def on_back_show(self, widget):
  1282. _assert_context(ui_context)
  1283. widget.hide()
  1284. def on_prepare(self, assistant, widget):
  1285. _assert_context(ui_context)
  1286. # If the user goes back then forward, we must ensure the feedback value to reportbug must be sent
  1287. # when the user clicks on "Forward" to the requested page by reportbug
  1288. if self.showing_page and self.showing_page == self.requested_page and self.get_current_page() > self.showing_page.page_num:
  1289. self.application.put_next_value()
  1290. # Reportbug doesn't support going back, so make widgets insensitive
  1291. self.showing_page.widget.set_sensitive(False)
  1292. self.showing_page.switch_out()
  1293. self.showing_page = widget.page
  1294. # Some pages might have changed the label in the while
  1295. if self.showing_page == self.progress_page:
  1296. self.progress_page.reset_label()
  1297. GLib.idle_add(self.showing_page.setup_focus)
  1298. def close(self, *args):
  1299. _assert_context(ui_context)
  1300. sys.exit(0)
  1301. def confirm_exit(self, *args):
  1302. _assert_context(ui_context)
  1303. dialog = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
  1304. Gtk.MessageType.WARNING, Gtk.ButtonsType.YES_NO,
  1305. "Are you sure you want to quit Reportbug?")
  1306. response = dialog.run()
  1307. dialog.destroy()
  1308. if response == Gtk.ResponseType.YES:
  1309. sys.exit(0)
  1310. def forward(self, page_num):
  1311. _assert_context(ui_context)
  1312. return page_num + 1
  1313. def forward_page(self):
  1314. _assert_context(ui_context)
  1315. self.set_current_page(self.forward(self.showing_page.page_num))
  1316. def set_next_page(self, page):
  1317. _assert_context(ui_context)
  1318. self.requested_page = page
  1319. # If we're in progress immediately show this guy
  1320. if self.showing_page == self.progress_page:
  1321. self.set_current_page(page.page_num)
  1322. def set_progress_label(self, text, *args, **kwargs):
  1323. _assert_context(ui_context)
  1324. self.progress_page.set_label(text % args)
  1325. def setup_pages(self):
  1326. _assert_context(ui_context)
  1327. # We insert pages between the intro and the progress, so that we give the user the feedback
  1328. # that the applications is still running when he presses the "Forward" button
  1329. self.showing_page = IntroPage(self)
  1330. self.showing_page.switch_in()
  1331. self.progress_page = ProgressPage(self)
  1332. self.progress_page.switch_in()
  1333. Page.next_page_num = 1
  1334. # Dialogs
  1335. class YesNoDialog(ReportbugConnector, Gtk.MessageDialog):
  1336. def __init__(self, application):
  1337. _assert_context(ui_context)
  1338. Gtk.MessageDialog.__init__(self, assistant, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
  1339. Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO)
  1340. self.application = application
  1341. self.connect('response', self.on_response)
  1342. def on_response(self, dialog, res):
  1343. _assert_context(ui_context)
  1344. self.application.set_next_value(res == Gtk.ResponseType.YES)
  1345. self.application.put_next_value()
  1346. self.destroy()
  1347. def execute_operation(self, msg, yeshelp=None, nohelp=None, default=True, nowrap=False):
  1348. _assert_context(ui_context)
  1349. self.set_markup(msg)
  1350. if default:
  1351. self.set_default_response(Gtk.ResponseType.YES)
  1352. else:
  1353. self.set_default_response(Gtk.ResponseType.NO)
  1354. self.show_all()
  1355. class DisplayFailureDialog(ReportbugConnector, Gtk.MessageDialog):
  1356. def __init__(self, application):
  1357. _assert_context(ui_context)
  1358. Gtk.MessageDialog.__init__(self, assistant, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
  1359. Gtk.MessageType.WARNING, Gtk.ButtonsType.CLOSE)
  1360. self.application = application
  1361. self.connect('response', self.on_response)
  1362. def on_response(self, dialog, res):
  1363. _assert_context(ui_context)
  1364. self.application.put_next_value()
  1365. self.destroy()
  1366. def execute_operation(self, msg, *args):
  1367. _assert_context(ui_context)
  1368. self.set_markup(msg % args)
  1369. self.show_all()
  1370. class GetFilenameDialog(ReportbugConnector, Gtk.FileChooserDialog):
  1371. def __init__(self, application):
  1372. _assert_context(ui_context)
  1373. Gtk.FileChooserDialog.__init__(self, '', assistant, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  1374. Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
  1375. self.application = application
  1376. self.connect('response', self.on_response)
  1377. def on_response(self, dialog, res):
  1378. _assert_context(ui_context)
  1379. value = None
  1380. if res == Gtk.ResponseType.OK:
  1381. value = self.get_filename()
  1382. self.application.set_next_value(value)
  1383. self.application.put_next_value()
  1384. self.destroy()
  1385. def execute_operation(self, title, force_prompt=False):
  1386. _assert_context(ui_context)
  1387. self.set_title(ask_free(title))
  1388. self.show_all()
  1389. def log_message(*args, **kwargs):
  1390. _assert_context(reportbug_context)
  1391. application.run_once_in_main_thread(assistant.set_progress_label, *args, **kwargs)
  1392. def select_multiple(*args, **kwargs):
  1393. _assert_context(reportbug_context)
  1394. kwargs['multiple'] = True
  1395. kwargs['empty_ok'] = True
  1396. return menu(*args, **kwargs)
  1397. def get_multiline(prompt, *args, **kwargs):
  1398. _assert_context(reportbug_context)
  1399. if 'ENTER' in prompt:
  1400. # This is a list, let's handle it the best way
  1401. return get_list(prompt, *args, **kwargs)
  1402. else:
  1403. return _get_multiline(prompt, *args, **kwargs)
  1404. pages = {'get_string': GetStringPage,
  1405. 'get_password': GetPasswordPage,
  1406. 'menu': MenuPage,
  1407. 'handle_bts_query': HandleBTSQueryPage,
  1408. 'show_report': ShowReportPage,
  1409. 'long_message': LongMessagePage,
  1410. 'display_report': DisplayReportPage,
  1411. 'final_message': FinalMessagePage,
  1412. 'spawn_editor': EditorPage,
  1413. 'select_options': SelectOptionsPage,
  1414. 'get_list': GetListPage,
  1415. 'system': SystemPage,
  1416. '_get_multiline': GetMultilinePage,
  1417. }
  1418. dialogs = {'yes_no': YesNoDialog,
  1419. 'get_filename': GetFilenameDialog,
  1420. 'display_failure': DisplayFailureDialog,
  1421. }
  1422. def create_forwarder(parent, klass):
  1423. _assert_context(reportbug_context)
  1424. def func(*args, **kwargs):
  1425. _assert_context(reportbug_context)
  1426. op = application.call_in_main_thread(klass, parent)
  1427. try:
  1428. args, kwargs = op.sync_pre_operation(*args, **kwargs)
  1429. except SyncReturn as e:
  1430. return e.result
  1431. application.run_once_in_main_thread(op.execute_operation, *args, **kwargs)
  1432. return application.get_last_value()
  1433. return func
  1434. def forward_operations(parent, operations):
  1435. _assert_context(reportbug_context)
  1436. for operation, klass in operations.items():
  1437. globals()[operation] = create_forwarder(parent, klass)
  1438. def initialize():
  1439. global application, assistant, reportbug_context, ui_context, Vte
  1440. try:
  1441. gi.require_version('Vte', '2.91')
  1442. from gi.repository import Vte
  1443. except ImportError:
  1444. message = """Please install the %s package to use the GTK+(known as 'gtk2' in reportbug) interface.
  1445. Falling back to 'text' interface."""
  1446. dialog = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
  1447. Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, None)
  1448. dialog.set_markup(message % "<b>gir1.2-vte-2.91</b>")
  1449. dialog.run()
  1450. dialog.destroy()
  1451. while Gtk.events_pending():
  1452. Gtk.main_iteration()
  1453. if not sys.stdout.isatty():
  1454. os.execlp('x-terminal-emulator', 'x-terminal-emulator', '-e', 'reportbug -u text')
  1455. return False
  1456. # The first thread of the process runs reportbug's UI-agnostic logic
  1457. reportbug_context = GLib.MainContext()
  1458. if not reportbug_context.acquire():
  1459. # should be impossible
  1460. raise AssertionError('Could not acquire new main-context')
  1461. reportbug_context.push_thread_default()
  1462. # A secondary thread (the ReportbugApplication) runs the GTK UI.
  1463. # This is the "default main context", used by GLib.idle_add() and similar
  1464. # non-thread-aware APIs.
  1465. ui_context = GLib.MainContext.default()
  1466. # Exception hook
  1467. oldhook = sys.excepthook
  1468. sys.excepthook = ExceptionDialog.create_excepthook(oldhook)
  1469. # GTK settings
  1470. Gtk.Window.set_default_icon_from_file(DEBIAN_LOGO)
  1471. application = ReportbugApplication()
  1472. application.start()
  1473. forward_operations(application, dialogs)
  1474. assistant = application.call_in_main_thread(ReportbugAssistant, application)
  1475. forward_operations(assistant, pages)
  1476. return True
  1477. def can_input():
  1478. _assert_context(reportbug_context)
  1479. return True