gtk2_ui.py 53 KB

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