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.
1290 lines
43 KiB
1290 lines
43 KiB
# a graphical (GTK+) user interface
|
|
# Written by Luca Bruno <lethalman88@gmail.com>
|
|
# Based on gnome-reportbug work done by Philipp Kern <pkern@debian.org>
|
|
# Copyright (C) 2006 Philipp Kern
|
|
# Copyright (C) 2008 Luca Bruno
|
|
#
|
|
# This program is freely distributable per the following license:
|
|
#
|
|
## Permission to use, copy, modify, and distribute this software and its
|
|
## documentation for any purpose and without fee is hereby granted,
|
|
## provided that the above copyright notice appears in all copies and that
|
|
## both that copyright notice and this permission notice appear in
|
|
## supporting documentation.
|
|
##
|
|
## I DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
|
|
## IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL I
|
|
## BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
|
|
## DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
|
## WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
|
## ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
|
|
## SOFTWARE.
|
|
#
|
|
# Version ##VERSION##; see changelog for revision history
|
|
|
|
try:
|
|
import gtk
|
|
from gtk import gdk
|
|
import gobject
|
|
except ImportError:
|
|
raise UINotImportable, 'Please install the python-gtk2 package to use this interface.'
|
|
|
|
gdk.threads_init ()
|
|
|
|
import sys
|
|
import re
|
|
import os
|
|
import traceback
|
|
from Queue import Queue
|
|
import threading
|
|
|
|
from reportbug.exceptions import NoPackage, NoBugs, NoNetwork, NoReport
|
|
from reportbug import debianbts
|
|
from reportbug.urlutils import launch_browser
|
|
|
|
ISATTY = True
|
|
DEBIAN_LOGO = "/usr/share/pixmaps/debian-logo.png"
|
|
|
|
global application, assistant
|
|
|
|
# Utilities
|
|
|
|
def highlight (s):
|
|
return '<b>%s</b>' % s
|
|
|
|
re_markup_free = re.compile ("<.*?>")
|
|
|
|
def markup_free (s):
|
|
return re_markup_free.sub ("", s)
|
|
|
|
def ask_free (s):
|
|
s = s.strip ()
|
|
if s[-1] in ('?', ':'):
|
|
return s[:-1]
|
|
return s
|
|
|
|
def create_scrollable (widget):
|
|
scrolled = gtk.ScrolledWindow ()
|
|
scrolled.set_shadow_type (gtk.SHADOW_ETCHED_IN)
|
|
scrolled.set_policy (gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
|
|
scrolled.add (widget)
|
|
return scrolled
|
|
|
|
def info_dialog (message):
|
|
dialog = gtk.MessageDialog (assistant, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
|
|
gtk.MESSAGE_INFO, gtk.BUTTONS_CLOSE, message)
|
|
dialog.connect ('response', lambda *args: dialog.destroy ())
|
|
dialog.set_title ('Reportbug')
|
|
dialog.show_all ()
|
|
|
|
def error_dialog (message):
|
|
dialog = gtk.MessageDialog (assistant, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
|
|
gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, message)
|
|
dialog.connect ('response', lambda *args: dialog.destroy ())
|
|
dialog.set_title ('Reportbug')
|
|
dialog.show_all ()
|
|
|
|
class CustomDialog (gtk.Dialog):
|
|
def __init__ (self, stock_image, message, buttons, *args, **kwargs):
|
|
gtk.Dialog.__init__ (self, "Reportbug", assistant,
|
|
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
|
|
buttons)
|
|
# Try following the HIG
|
|
self.set_default_response (buttons[-1]) # this is the response of the last button
|
|
self.set_border_width (5)
|
|
|
|
vbox = gtk.VBox (spacing=10)
|
|
vbox.set_border_width (6)
|
|
self.vbox.pack_start (vbox)
|
|
|
|
# The header image + label
|
|
hbox = gtk.HBox (spacing=10)
|
|
vbox.pack_start (hbox, expand=False)
|
|
|
|
align = gtk.Alignment (0.5, 0.5, 1.0, 1.0)
|
|
hbox.pack_start (align, expand=False)
|
|
|
|
image = gtk.image_new_from_stock (stock_image, gtk.ICON_SIZE_DIALOG)
|
|
hbox.pack_start (image)
|
|
|
|
label = gtk.Label (message)
|
|
label.set_line_wrap (True)
|
|
label.set_justify (gtk.JUSTIFY_FILL)
|
|
hbox.pack_start (label, expand=False)
|
|
|
|
self.setup_dialog (vbox, *args, **kwargs)
|
|
|
|
class InputStringDialog (CustomDialog):
|
|
def __init__ (self, message):
|
|
CustomDialog.__init__ (self, gtk.STOCK_DIALOG_INFO, message,
|
|
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
|
|
gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
|
|
|
|
def setup_dialog (self, vbox):
|
|
self.entry = gtk.Entry ()
|
|
vbox.pack_start (self.entry, expand=False)
|
|
|
|
def get_value (self):
|
|
return self.entry.get_text ()
|
|
|
|
class ExceptionDialog (CustomDialog):
|
|
# Register an exception hook to display an error when the GUI breaks
|
|
@classmethod
|
|
def create_excepthook (cls, oldhook):
|
|
def excepthook (exctype, value, tb):
|
|
if oldhook:
|
|
oldhook (exctype, value, tb)
|
|
application.run_once_in_main_thread (cls.start_dialog,
|
|
''.join (traceback.format_exception (exctype, value, tb)))
|
|
return excepthook
|
|
|
|
@classmethod
|
|
def start_dialog (cls, tb):
|
|
try:
|
|
dialog = cls (tb)
|
|
dialog.show_all ()
|
|
except:
|
|
sys.exit (1)
|
|
|
|
def __init__ (self, tb):
|
|
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)
|
|
|
|
def setup_dialog (self, vbox, tb):
|
|
# The traceback
|
|
expander = gtk.Expander ("More details")
|
|
vbox.pack_start (expander)
|
|
|
|
view = gtk.TextView ()
|
|
view.set_editable (False)
|
|
view.get_buffer().set_text (tb)
|
|
scrolled = create_scrollable (view)
|
|
expander.add (scrolled)
|
|
|
|
self.connect ('response', self.on_response)
|
|
|
|
def on_response (self, dialog, res):
|
|
sys.exit (1)
|
|
|
|
# BTS
|
|
|
|
class Bug (object):
|
|
def __init__ (self, raw):
|
|
# Skip the '#'
|
|
raw = raw[1:]
|
|
bits = re.split(r'[: ]', raw, 2)
|
|
self.id, self.tag, self.data = bits
|
|
# Remove [ and ]
|
|
self.tag = self.tag[1:-1]
|
|
self.data = self.data.strip ()
|
|
self.package = self.data.split(']', 1)[0][1:]
|
|
|
|
self.reporter = self.get_data ("Reported by:")
|
|
self.date = self.get_data ("Date:")
|
|
self.severity = self.get_data("Severity:").capitalize ()
|
|
self.version = self.get_data ("Found in version")
|
|
self.filed_date = self.get_data ("Filed")
|
|
self.modified_date = self.get_data ("Modified")
|
|
|
|
# Get rid of [package] which has been stored in self.package
|
|
self.info = self.data.split(']', 1)[1]
|
|
self.info = self.info[:self.info.lower().index ("reported by:")].strip ()
|
|
if not self.info:
|
|
self.info = '(no subject)'
|
|
|
|
def get_data (self, token):
|
|
info = ''
|
|
try:
|
|
index = self.data.lower().index (token.lower ())
|
|
except:
|
|
return '(unknown)'
|
|
|
|
i = index + len(token)
|
|
while True:
|
|
c = self.data[i]
|
|
if c == ';':
|
|
break
|
|
info += c
|
|
i += 1
|
|
return info.strip ()
|
|
|
|
def __iter__ (self):
|
|
yield self.id
|
|
yield self.tag
|
|
yield self.package
|
|
yield self.info
|
|
yield self.reporter
|
|
yield self.date
|
|
yield self.severity
|
|
yield self.version
|
|
yield self.filed_date
|
|
yield self.modified_date
|
|
|
|
class BugReport (object):
|
|
def __init__ (self, message):
|
|
lines = message.split ('\n')
|
|
i = 0
|
|
|
|
self.headers = []
|
|
while i < len (lines):
|
|
line = lines[i]
|
|
i += 1
|
|
if not line.strip ():
|
|
break
|
|
self.headers.append (line)
|
|
|
|
store = 0
|
|
info = []
|
|
while i < len (lines):
|
|
line = lines[i]
|
|
info.append (line)
|
|
i += 1
|
|
if store < 2 and not line.strip():
|
|
store += 1
|
|
continue
|
|
if store == 2 and (line.startswith ('-- ') or line.startswith ('** ')):
|
|
break
|
|
store = 0
|
|
self.original_info = '\n'.join (info[:-3])
|
|
|
|
self.others = '\n'.join (lines[i-1:])
|
|
|
|
def get_others (self):
|
|
return self.others
|
|
|
|
def get_original_info (self):
|
|
return self.original_info
|
|
|
|
def get_subject (self):
|
|
for header in self.headers:
|
|
if 'Subject' in header:
|
|
return header[len ('Subject: '):]
|
|
|
|
def set_subject (self, subject):
|
|
for i in range (len (self.headers)):
|
|
if 'Subject' in self.headers[i]:
|
|
self.headers[i] = 'Subject: '+subject
|
|
break
|
|
|
|
def create_message (self, info):
|
|
message = """%s
|
|
|
|
%s
|
|
|
|
|
|
%s""" % ('\n'.join (self.headers), info,self.others)
|
|
return message
|
|
|
|
# BTS GUI
|
|
|
|
class BugPage (gtk.EventBox, threading.Thread):
|
|
def __init__ (self, dialog, number, queryonly, bts, mirrors, http_proxy, archived):
|
|
threading.Thread.__init__ (self)
|
|
gtk.EventBox.__init__ (self)
|
|
self.dialog = dialog
|
|
self.assistant = self.dialog.assistant
|
|
self.application = self.assistant.application
|
|
self.number = number
|
|
self.queryonly = queryonly
|
|
self.bts = bts
|
|
self.mirrors = mirrors
|
|
self.http_proxy = http_proxy
|
|
self.archived = archived
|
|
|
|
vbox = gtk.VBox (spacing=12)
|
|
vbox.pack_start (gtk.Label ("Retrieving bug information."), expand=False)
|
|
|
|
self.progress = gtk.ProgressBar ()
|
|
self.progress.set_pulse_step (0.01)
|
|
vbox.pack_start (self.progress, expand=False)
|
|
|
|
self.add (vbox)
|
|
|
|
def run (self):
|
|
# Start the progress bar
|
|
gobject.timeout_add (10, self.pulse)
|
|
|
|
info = debianbts.get_report (int (self.number), self.bts, mirrors=self.mirrors,
|
|
http_proxy=self.http_proxy, archived=self.archived)
|
|
if not info:
|
|
self.application.run_once_in_main_thread (self.not_found)
|
|
else:
|
|
self.application.run_once_in_main_thread (self.found, info)
|
|
|
|
def drop_progressbar (self):
|
|
child = self.get_child ()
|
|
self.remove (child)
|
|
child.unparent ()
|
|
|
|
def pulse (self):
|
|
self.progress.pulse ()
|
|
return self.isAlive ()
|
|
|
|
def not_found (self):
|
|
self.drop_progressbar ()
|
|
self.add (gtk.Label ("The bug can't be fetched or it doesn't exist."))
|
|
self.show_all ()
|
|
|
|
def found (self, info):
|
|
self.drop_progressbar ()
|
|
desc = info[0].split(':', 1)[1].strip ()
|
|
bodies = info[1]
|
|
vbox = gtk.VBox (spacing=12)
|
|
vbox.set_border_width (12)
|
|
label = gtk.Label ('Description: '+desc)
|
|
label.set_line_wrap (True)
|
|
vbox.pack_start (label, expand=False)
|
|
|
|
view = gtk.TreeView ()
|
|
view.get_selection().set_mode (gtk.SELECTION_NONE)
|
|
view.set_rules_hint (True)
|
|
model = gtk.ListStore (str)
|
|
view.set_model (model)
|
|
view.append_column (gtk.TreeViewColumn ('Replies', gtk.CellRendererText (), text=0))
|
|
for body in bodies:
|
|
model.append ([body])
|
|
scrolled = create_scrollable (view)
|
|
vbox.pack_start (scrolled)
|
|
|
|
bbox = gtk.HButtonBox ()
|
|
button = gtk.Button ("Open in browser")
|
|
button.connect ('clicked', self.on_open_browser)
|
|
bbox.pack_start (button)
|
|
if not self.queryonly:
|
|
button = gtk.Button ("Reply")
|
|
button.set_image (gtk.image_new_from_stock (gtk.STOCK_EDIT, gtk.ICON_SIZE_BUTTON))
|
|
button.connect ('clicked', self.on_reply)
|
|
bbox.pack_start (button)
|
|
vbox.pack_start (bbox, expand=False)
|
|
|
|
self.add (vbox)
|
|
self.show_all ()
|
|
|
|
def on_open_browser (self, button):
|
|
launch_browser (debianbts.get_report_url (self.bts, int (self.number), self.archived))
|
|
|
|
def on_reply (self, button):
|
|
# Return the bug number to reportbug
|
|
self.application.set_next_value (self.number)
|
|
# Forward the assistant to the progress bar
|
|
self.assistant.forward_page ()
|
|
# Though we only a page, we are authorized to destroy our parent :)
|
|
self.dialog.destroy ()
|
|
|
|
class BugsDialog (gtk.Dialog):
|
|
def __init__ (self, assistant, queryonly):
|
|
gtk.Dialog.__init__ (self, "Reportbug: bug information", assistant,
|
|
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
|
|
(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE))
|
|
self.assistant = assistant
|
|
self.queryonly = queryonly
|
|
self.application = assistant.application
|
|
self.notebook = gtk.Notebook ()
|
|
self.vbox.pack_start (self.notebook)
|
|
self.connect ('response', self.on_response)
|
|
self.set_default_size (600, 600)
|
|
|
|
def on_response (self, *args):
|
|
self.destroy ()
|
|
|
|
def show_bug (self, number, *args):
|
|
page = BugPage (self, number, self.queryonly, *args)
|
|
self.notebook.append_page (page, gtk.Label (number))
|
|
page.start ()
|
|
|
|
# Application
|
|
|
|
class ReportbugApplication (threading.Thread):
|
|
def __init__ (self):
|
|
threading.Thread.__init__ (self)
|
|
self.queue = Queue ()
|
|
self.next_value = None
|
|
|
|
def run (self):
|
|
gtk.main ()
|
|
|
|
def get_last_value (self):
|
|
return self.queue.get ()
|
|
|
|
def put_next_value (self):
|
|
self.queue.put (self.next_value)
|
|
self.next_value = None
|
|
|
|
def set_next_value (self, value):
|
|
self.next_value = value
|
|
|
|
@staticmethod
|
|
def create_idle_callback (func, *args, **kwargs):
|
|
def callback ():
|
|
func (*args, **kwargs)
|
|
return False
|
|
return callback
|
|
|
|
def run_once_in_main_thread (self, func, *args, **kwargs):
|
|
gobject.idle_add (self.create_idle_callback (func, *args, **kwargs))
|
|
|
|
# Connection with reportbug
|
|
|
|
# Syncronize "pipe" with reportbug
|
|
|
|
class SyncReturn (RuntimeError):
|
|
def __init__ (self, result):
|
|
RuntimeError.__init__ (self, result)
|
|
self.result = result
|
|
|
|
class ReportbugConnector (object):
|
|
# Executed in the glib thread
|
|
def execute_operation (self, *args, **kwargs):
|
|
pass
|
|
|
|
# Executed in sync with reportbug. raise SyncResult (value) to directly return to reportbug
|
|
# Returns args and kwargs to pass to execute_operation
|
|
def sync_pre_operation (cls, *args, **kwargs):
|
|
return args, kwargs
|
|
|
|
# Assistant
|
|
|
|
class Page (ReportbugConnector):
|
|
next_page_num = 0
|
|
page_type = gtk.ASSISTANT_PAGE_CONTENT
|
|
default_complete = False
|
|
side_image = DEBIAN_LOGO
|
|
|
|
def __init__ (self, assistant):
|
|
self.assistant = assistant
|
|
self.application = assistant.application
|
|
self.widget = self.create_widget ()
|
|
self.widget.page = self
|
|
self.widget.set_border_width (6)
|
|
self.widget.show_all ()
|
|
self.page_num = Page.next_page_num
|
|
|
|
def execute_operation (self, *args, **kwargs):
|
|
self.switch_in ()
|
|
self.connect_signals ()
|
|
self.empty_ok = kwargs.pop ('empty_ok', False)
|
|
self.execute (*args, **kwargs)
|
|
self.assistant.show ()
|
|
|
|
def connect_signals (self):
|
|
pass
|
|
|
|
def set_page_complete (self, complete):
|
|
self.assistant.set_page_complete (self.widget, complete)
|
|
|
|
def set_page_type (self, type):
|
|
self.assistant.set_page_type (self.widget, type)
|
|
|
|
def set_page_title (self, title):
|
|
if title:
|
|
self.assistant.set_page_title (self.widget, title)
|
|
|
|
# The user will see this as next page
|
|
def switch_in (self):
|
|
Page.next_page_num += 1
|
|
self.assistant.insert_page (self.widget, self.page_num)
|
|
self.set_page_complete (self.default_complete)
|
|
self.set_page_type (self.page_type)
|
|
self.assistant.set_page_side_image (self.widget, gdk.pixbuf_new_from_file (self.side_image))
|
|
self.assistant.set_next_page (self)
|
|
self.set_page_title ("Reportbug")
|
|
|
|
# Setup keyboard focus in the page
|
|
def setup_focus (self):
|
|
self.widget.grab_focus ()
|
|
|
|
# The user forwarded the assistant to see the next page
|
|
def switch_out (self):
|
|
pass
|
|
|
|
def is_valid (self, value):
|
|
if self.empty_ok:
|
|
return True
|
|
else:
|
|
return bool (value)
|
|
|
|
def validate (self, *args, **kwargs):
|
|
value = self.get_value ()
|
|
if self.is_valid (value):
|
|
self.application.set_next_value (value)
|
|
self.set_page_complete (True)
|
|
else:
|
|
self.set_page_complete (False)
|
|
|
|
class IntroPage (Page):
|
|
page_type = gtk.ASSISTANT_PAGE_INTRO
|
|
default_complete = True
|
|
|
|
def create_widget (self):
|
|
vbox = gtk.VBox (spacing=24)
|
|
|
|
label = gtk.Label ("""
|
|
<b>Reportbug</b> is a tool designed to make the reporting of bugs in Debian and derived distributions relatively painless.
|
|
|
|
This wizard will guide you through the bug reporting process step by step.""")
|
|
label.set_use_markup (True)
|
|
label.set_line_wrap (True)
|
|
label.set_justify (gtk.JUSTIFY_FILL)
|
|
vbox.pack_start (label, expand=False)
|
|
|
|
link = gtk.LinkButton ("http://alioth.debian.org/projects/reportbug",
|
|
"Homepage of reportbug project")
|
|
vbox.pack_start (link, expand=False)
|
|
return vbox
|
|
|
|
class GetStringPage (Page):
|
|
def setup_focus (self):
|
|
self.entry.grab_focus ()
|
|
|
|
def create_widget (self):
|
|
vbox = gtk.VBox (spacing=12)
|
|
self.label = gtk.Label ()
|
|
self.label.set_line_wrap (True)
|
|
self.entry = gtk.Entry ()
|
|
vbox.pack_start (self.label, expand=False)
|
|
vbox.pack_start (self.entry, expand=False)
|
|
return vbox
|
|
|
|
def connect_signals (self):
|
|
self.entry.connect ('changed', self.validate)
|
|
|
|
def get_value (self):
|
|
return self.entry.get_text ()
|
|
|
|
def execute (self, prompt, options=None, force_prompt=False, default=''):
|
|
if 'blank OK' in prompt:
|
|
self.empty_ok = True
|
|
else:
|
|
self.empty_ok = False
|
|
self.label.set_text (prompt)
|
|
self.entry.set_text (default)
|
|
|
|
if options:
|
|
options.sort ()
|
|
completion = gtk.EntryCompletion ()
|
|
model = gtk.ListStore (str)
|
|
for option in options:
|
|
model.append ([option])
|
|
completion.set_model (model)
|
|
completion.set_inline_selection (True)
|
|
completion.set_text_column (0)
|
|
self.entry.set_completion (completion)
|
|
else:
|
|
self.completion = None
|
|
|
|
self.validate ()
|
|
|
|
class GetMultilinePage (Page):
|
|
def setup_focus (self):
|
|
self.view.grab_focus ()
|
|
|
|
def create_widget (self):
|
|
vbox = gtk.VBox (spacing=12)
|
|
self.label = gtk.Label ()
|
|
vbox.pack_start (self.label, expand=False)
|
|
|
|
self.view = gtk.TextView ()
|
|
self.buffer = view.get_buffer ()
|
|
scrolled = create_scrollable (self.view)
|
|
vbox.pack_start (scrolled)
|
|
return vbox
|
|
|
|
def connect_signals (self):
|
|
self.buffer.connect ('changed', self.validate)
|
|
|
|
def get_value (self):
|
|
text = self.buffer.get_text (self.buffer.get_start_iter (), self.buffer.get_end_iter ())
|
|
lines = text.split ('\n')
|
|
# Remove the trailing empty line at the end
|
|
if len (lines) > 0 and not lines[-1].strip ():
|
|
del lines[-1]
|
|
return text.split ('\n')
|
|
|
|
def execute (self, prompt):
|
|
self.empty_ok = True
|
|
# The result must be iterable for reportbug even if it's empty and not modified
|
|
self.label.set_text (prompt)
|
|
self.buffer.set_text ("")
|
|
self.buffer.emit ('changed')
|
|
|
|
class TreePage (Page):
|
|
value_column = None
|
|
|
|
def __init__ (self, *args, **kwargs):
|
|
Page.__init__ (self, *args, **kwargs)
|
|
self.selection = self.view.get_selection()
|
|
|
|
def setup_focus (self):
|
|
self.view.grab_focus ()
|
|
|
|
def connect_signals (self):
|
|
self.selection.connect ('changed', self.validate)
|
|
|
|
def get_value (self):
|
|
model, paths = self.selection.get_selected_rows ()
|
|
multiple = self.selection.get_mode () == gtk.SELECTION_MULTIPLE
|
|
result = []
|
|
for path in paths:
|
|
value = model.get_value (model.get_iter (path), self.value_column)
|
|
if value is not None:
|
|
result.append (markup_free (value))
|
|
if result and not multiple:
|
|
return result[0]
|
|
return result
|
|
|
|
class GetListPage (TreePage):
|
|
value_column = 0
|
|
|
|
def create_widget (self):
|
|
vbox = gtk.VBox (spacing=12)
|
|
self.label = gtk.Label ()
|
|
vbox.pack_start (self.label, expand=False)
|
|
|
|
hbox = gtk.HBox (spacing=6)
|
|
|
|
self.view = gtk.TreeView ()
|
|
self.view.set_rules_hint (True)
|
|
self.view.get_selection().set_mode (gtk.SELECTION_MULTIPLE)
|
|
scrolled = create_scrollable (self.view)
|
|
hbox.pack_start (scrolled)
|
|
|
|
bbox = gtk.VButtonBox ()
|
|
bbox.set_spacing (6)
|
|
bbox.set_layout (gtk.BUTTONBOX_START)
|
|
button = gtk.Button (stock=gtk.STOCK_ADD)
|
|
button.connect ('clicked', self.on_add)
|
|
bbox.pack_start (button, expand=False)
|
|
button = gtk.Button (stock=gtk.STOCK_REMOVE)
|
|
button.connect ('clicked', self.on_remove)
|
|
bbox.pack_start (button, expand=False)
|
|
hbox.pack_start (bbox, expand=False)
|
|
|
|
vbox.pack_start (hbox)
|
|
return vbox
|
|
|
|
def get_value (self):
|
|
values = []
|
|
for row in self.model:
|
|
values.append (row[self.value_column])
|
|
return values
|
|
|
|
def on_add (self, button):
|
|
dialog = InputStringDialog ("Add a new item to the list")
|
|
dialog.show_all ()
|
|
dialog.connect ('response', self.on_add_dialog_response)
|
|
|
|
def on_add_dialog_response (self, dialog, res):
|
|
if res == gtk.RESPONSE_ACCEPT:
|
|
self.model.append ([dialog.get_value ()])
|
|
dialog.destroy ()
|
|
|
|
def on_remove (self, button):
|
|
model, paths = self.selection.get_selected_rows ()
|
|
# We need to transform them to iters, since paths change when removing rows
|
|
iters = []
|
|
for path in paths:
|
|
iters.append (self.model.get_iter (path))
|
|
for iter in iters:
|
|
self.model.remove (iter)
|
|
|
|
def execute (self, prompt):
|
|
self.empty_ok = True
|
|
|
|
self.label.set_text (prompt)
|
|
|
|
self.model = gtk.ListStore (str)
|
|
self.model.connect ('row-changed', self.validate)
|
|
self.view.set_model (self.model)
|
|
|
|
self.selection.set_mode (gtk.SELECTION_MULTIPLE)
|
|
|
|
self.view.append_column (gtk.TreeViewColumn ('Item', gtk.CellRendererText (), text=0))
|
|
|
|
class MenuPage (TreePage):
|
|
value_column = 0
|
|
|
|
def create_widget (self):
|
|
vbox = gtk.VBox (spacing=12)
|
|
self.label = gtk.Label ()
|
|
vbox.pack_start (self.label, expand=False)
|
|
|
|
self.view = gtk.TreeView ()
|
|
self.view.set_rules_hint (True)
|
|
scrolled = create_scrollable (self.view)
|
|
vbox.pack_start (scrolled)
|
|
vbox.show_all ()
|
|
return vbox
|
|
|
|
def execute (self, par, options, prompt, default=None, any_ok=False,
|
|
order=None, extras=None, multiple=False):
|
|
self.label.set_text (par)
|
|
|
|
self.model = gtk.ListStore (str, str)
|
|
self.view.set_model (self.model)
|
|
|
|
if multiple:
|
|
self.selection.set_mode (gtk.SELECTION_MULTIPLE)
|
|
|
|
self.view.append_column (gtk.TreeViewColumn ('Option', gtk.CellRendererText (), markup=0))
|
|
self.view.append_column (gtk.TreeViewColumn ('Description', gtk.CellRendererText (), text=1))
|
|
|
|
default_iter = None
|
|
if isinstance (options, dict):
|
|
for option, desc in options.iteritems ():
|
|
iter = self.model.append ((highlight (option), desc))
|
|
if option == default:
|
|
default_iter = iter
|
|
else:
|
|
for row in options:
|
|
iter = self.model.append ((highlight (row[0]), row[1]))
|
|
if row[0] == default:
|
|
default_iter = iter
|
|
|
|
if default_iter:
|
|
self.selection.select_iter (default_iter)
|
|
|
|
class HandleBTSQueryPage (TreePage):
|
|
default_complete = True
|
|
value_column = 0
|
|
|
|
def sync_pre_operation (self, package, bts, mirrors=None, http_proxy="", queryonly=False, screen=None,
|
|
archived='no', source=False, title=None, version=None):
|
|
self.bts = bts
|
|
self.mirrors = mirrors
|
|
self.http_proxy = http_proxy
|
|
self.archived = archived
|
|
|
|
self.queryonly = queryonly
|
|
if queryonly:
|
|
self.page_type = gtk.ASSISTANT_PAGE_CONFIRM
|
|
|
|
sysinfo = debianbts.SYSTEMS[bts]
|
|
root = sysinfo.get('btsroot')
|
|
if not root:
|
|
# do we need to make a dialog for this?
|
|
return
|
|
|
|
if isinstance(package, basestring):
|
|
pkgname = package
|
|
if source:
|
|
pkgname += ' (source)'
|
|
|
|
progress_label = 'Querying %s bug tracking system for reports on %s' % (debianbts.SYSTEMS[bts]['name'], pkgname)
|
|
else:
|
|
progress_label = 'Querying %s bug tracking system for reports %s' % (debianbts.SYSTEMS[bts]['name'], ' '.join([str(x) for x in package]))
|
|
|
|
|
|
self.application.run_once_in_main_thread (self.assistant.set_progress_label, progress_label)
|
|
|
|
try:
|
|
(count, sectitle, hierarchy) = debianbts.get_reports (
|
|
package, bts, mirrors=mirrors, version=version,
|
|
http_proxy=http_proxy, archived=archived, source=source)
|
|
|
|
if not count:
|
|
if hierarchy == None:
|
|
raise NoPackage
|
|
else:
|
|
raise NoBugs
|
|
else:
|
|
if count > 1:
|
|
sectitle = '%d bug reports found' % (count,)
|
|
else:
|
|
sectitle = 'One bug report found'
|
|
|
|
report = []
|
|
for category, bugs in hierarchy:
|
|
buglist = []
|
|
for bug in bugs:
|
|
buglist.append (Bug (bug))
|
|
report.append ((category, buglist))
|
|
|
|
return (report, sectitle), {}
|
|
|
|
except (IOError, NoNetwork):
|
|
error_dialog ("Unable to connect to %s BTS." % sysinfo['name'])
|
|
except NoPackage:
|
|
error_dialog ('No record of this package found.')
|
|
raise NoPackage
|
|
|
|
raise SyncReturn (None)
|
|
|
|
def setup_focus (self):
|
|
self.entry.grab_focus ()
|
|
|
|
def create_widget (self):
|
|
vbox = gtk.VBox (spacing=6)
|
|
self.label = gtk.Label ("List of bugs. Select a bug to retrieve and submit more information.")
|
|
vbox.pack_start (self.label, expand=False, padding=6)
|
|
|
|
hbox = gtk.HBox (spacing=6)
|
|
label = gtk.Label ("Filter:")
|
|
hbox.pack_start (label, expand=False)
|
|
self.entry = gtk.Entry ()
|
|
hbox.pack_start (self.entry)
|
|
button = gtk.Button ()
|
|
button.set_image (gtk.image_new_from_stock (gtk.STOCK_CLEAR, gtk.ICON_SIZE_BUTTON))
|
|
button.connect ('clicked', self.on_filter_clear)
|
|
hbox.pack_start (button, expand=False)
|
|
vbox.pack_start (hbox, expand=False)
|
|
|
|
self.view = gtk.TreeView ()
|
|
self.view.set_rules_hint (True)
|
|
scrolled = create_scrollable (self.view)
|
|
self.columns = ['ID', 'Tag', 'Package', 'Description', 'Reporter', 'Date', 'Severity', 'Version',
|
|
'Filed date', 'Modified date']
|
|
for col in zip (self.columns, range (len (self.columns))):
|
|
self.view.append_column (gtk.TreeViewColumn (col[0], gtk.CellRendererText (), text=col[1]))
|
|
vbox.pack_start (scrolled)
|
|
|
|
button = gtk.Button ("Retrieve and submit bug information")
|
|
button.set_image (gtk.image_new_from_stock (gtk.STOCK_INFO, gtk.ICON_SIZE_BUTTON))
|
|
button.connect ('clicked', self.on_retrieve_info)
|
|
vbox.pack_start (button, expand=False)
|
|
return vbox
|
|
|
|
def connect_signals (self):
|
|
TreePage.connect_signals (self)
|
|
self.view.connect ('row-activated', self.on_retrieve_info)
|
|
self.entry.connect ('changed', self.on_filter_changed)
|
|
|
|
def on_filter_clear (self, button):
|
|
self.entry.set_text ("")
|
|
|
|
def on_filter_changed (self, entry):
|
|
self.model.filter_text = entry.get_text().lower ()
|
|
self.filter.refilter ()
|
|
|
|
def on_retrieve_info (self, *args):
|
|
bug_ids = TreePage.get_value (self)
|
|
if not bug_ids:
|
|
info_dialog ("Please select one ore more bugs")
|
|
return
|
|
|
|
dialog = BugsDialog (self.assistant, self.queryonly)
|
|
for id in bug_ids:
|
|
dialog.show_bug (id, self.bts, self.mirrors, self.http_proxy, self.archived)
|
|
dialog.show_all ()
|
|
|
|
def is_valid (self, value):
|
|
return True
|
|
|
|
def get_value (self):
|
|
# The value returned to reportbug doesn't depend by a selection, but by the dialog of a bug
|
|
return None
|
|
|
|
def match_filter (self, iter):
|
|
# Flatten the columns into a single string
|
|
text = ""
|
|
for col in range (len (self.columns)):
|
|
value = self.model.get_value (iter, col)
|
|
if value:
|
|
text += self.model.get_value (iter, col) + " "
|
|
|
|
text = text.lower ()
|
|
# Tokens shouldn't be adjacent by default
|
|
for token in self.model.filter_text.split (' '):
|
|
if token in text:
|
|
return True
|
|
return False
|
|
|
|
def filter_visible_func (self, model, iter):
|
|
matches = self.match_filter (iter)
|
|
if not self.model.iter_parent (iter) and not matches:
|
|
# If no children are visible, hide it
|
|
it = model.iter_children (iter)
|
|
while it:
|
|
if self.match_filter (it):
|
|
return True
|
|
it = model.iter_next (it)
|
|
return False
|
|
|
|
return matches
|
|
|
|
def execute (self, buglist, sectitle):
|
|
self.label.set_text ("%s. Double-click a bug to retrieve and submit more information." % sectitle)
|
|
|
|
self.model = gtk.TreeStore (*([str] * len (self.columns)))
|
|
for category in buglist:
|
|
row = [None] * len (self.columns)
|
|
row[3] = category[0]
|
|
iter = self.model.append (None, row)
|
|
for bug in category[1]:
|
|
self.model.append (iter, list (bug))
|
|
|
|
self.selection.set_mode (gtk.SELECTION_MULTIPLE)
|
|
|
|
self.model.filter_text = ""
|
|
self.filter = self.model.filter_new ()
|
|
self.filter.set_visible_func (self.filter_visible_func)
|
|
self.view.set_model (self.filter)
|
|
|
|
class DisplayReportPage (Page):
|
|
default_complete = True
|
|
|
|
def create_widget (self):
|
|
self.view = gtk.TextView ()
|
|
self.view.set_editable (False)
|
|
scrolled = create_scrollable (self.view)
|
|
return scrolled
|
|
|
|
def execute (self, message, *args):
|
|
self.view.get_buffer().set_text (message % args)
|
|
|
|
class LongMessagePage (Page):
|
|
default_complete = True
|
|
|
|
def create_widget (self):
|
|
self.label = gtk.Label ()
|
|
self.label.set_line_wrap (True)
|
|
self.label.set_justify (gtk.JUSTIFY_FILL)
|
|
eb = gtk.EventBox ()
|
|
eb.add (self.label)
|
|
return eb
|
|
|
|
def execute (self, message, *args):
|
|
message = message % args
|
|
self.label.set_text (message)
|
|
# Reportbug should use final_message, so emulate it
|
|
if ('999999' in message):
|
|
self.set_page_type (gtk.ASSISTANT_PAGE_CONFIRM)
|
|
self.set_page_title ("Thanks for your report")
|
|
|
|
class FinalMessagePage (LongMessagePage):
|
|
page_type = gtk.ASSISTANT_PAGE_CONFIRM
|
|
default_complete = True
|
|
|
|
def execute (self, *args, **kwargs):
|
|
LongMessagePage.execute (self, *args, **kwargs)
|
|
self.set_page_title ("Thanks for your report")
|
|
|
|
class EditorPage (Page):
|
|
def create_widget (self):
|
|
vbox = gtk.VBox (spacing=6)
|
|
hbox = gtk.HBox (spacing=12)
|
|
hbox.pack_start (gtk.Label ("Subject: "), expand=False)
|
|
self.subject = gtk.Entry ()
|
|
hbox.pack_start (self.subject)
|
|
vbox.pack_start (hbox, expand=False)
|
|
|
|
self.view = gtk.TextView ()
|
|
self.info_buffer = self.view.get_buffer ()
|
|
scrolled = create_scrollable (self.view)
|
|
vbox.pack_start (scrolled)
|
|
|
|
expander = gtk.Expander ("Other system information")
|
|
view = gtk.TextView ()
|
|
view.set_editable (False)
|
|
self.others_buffer = view.get_buffer ()
|
|
scrolled = create_scrollable (view)
|
|
expander.add (scrolled)
|
|
vbox.pack_start (expander)
|
|
return vbox
|
|
|
|
def switch_out (self):
|
|
f = file (self.filename, "w")
|
|
f.write (self.get_value()[0])
|
|
f.close ()
|
|
|
|
def connect_signals (self):
|
|
self.info_buffer.connect ('changed', self.validate)
|
|
self.subject.connect ('changed', self.validate)
|
|
|
|
def get_value (self):
|
|
info = self.info_buffer.get_text (self.info_buffer.get_start_iter (),
|
|
self.info_buffer.get_end_iter ())
|
|
if not info.strip ():
|
|
return None
|
|
subject = self.subject.get_text().strip ()
|
|
if not subject.strip ():
|
|
return None
|
|
|
|
self.report.set_subject (subject)
|
|
message = self.report.create_message (info)
|
|
message = message.decode (self.charset, 'replace')
|
|
return (message, message != self.message)
|
|
|
|
def handle_first_info (self):
|
|
self.focus_in_id = self.view.connect ('focus-in-event', self.on_view_focus_in_event)
|
|
|
|
def on_view_focus_in_event (self, view, *args):
|
|
# Empty the buffer only the first time
|
|
self.info_buffer.set_text ("")
|
|
view.disconnect (self.focus_in_id)
|
|
|
|
def execute (self, message, filename, editor, charset='utf-8'):
|
|
self.message = message
|
|
self.report = BugReport (message)
|
|
self.filename = filename
|
|
self.charset = charset
|
|
self.subject.set_text (self.report.get_subject ())
|
|
self.others_buffer.set_text (self.report.get_others ())
|
|
|
|
info = self.report.get_original_info ()
|
|
if info.strip () == "*** Please type your report below this line ***":
|
|
info = "Please type your report here..."
|
|
self.handle_first_info ()
|
|
self.info_buffer.set_text (info)
|
|
|
|
class SelectOptionsPage (Page):
|
|
default_complete = False
|
|
|
|
def create_widget (self):
|
|
self.label = gtk.Label ()
|
|
self.vbox = gtk.VBox (spacing=6)
|
|
self.vbox.pack_start (self.label, expand=False, padding=6)
|
|
self.default = None
|
|
return self.vbox
|
|
|
|
def on_clicked (self, button, menuopt):
|
|
self.application.set_next_value (menuopt)
|
|
self.assistant.forward_page ()
|
|
|
|
def setup_focus (self):
|
|
if self.default:
|
|
self.default.set_flags (gtk.CAN_DEFAULT | gtk.HAS_DEFAULT)
|
|
self.default.grab_default ()
|
|
self.default.grab_focus ()
|
|
|
|
def execute (self, prompt, menuopts, options):
|
|
self.label.set_text (prompt)
|
|
|
|
buttons = []
|
|
for menuopt in menuopts:
|
|
desc = options[menuopt.lower ()]
|
|
# do we really need to launch an external editor?
|
|
if 'Change editor' in desc:
|
|
continue
|
|
button = gtk.Button (options[menuopt.lower ()])
|
|
button.connect ('clicked', self.on_clicked, menuopt.lower ())
|
|
if menuopt.isupper ():
|
|
self.default = button
|
|
buttons.insert (0, gtk.HSeparator ())
|
|
buttons.insert (0, button)
|
|
else:
|
|
buttons.append (button)
|
|
|
|
for button in buttons:
|
|
self.vbox.pack_start (button, expand=False)
|
|
|
|
self.vbox.show_all ()
|
|
|
|
class ProgressPage (Page):
|
|
page_type = gtk.ASSISTANT_PAGE_PROGRESS
|
|
|
|
def pulse (self):
|
|
self.progress.pulse ()
|
|
return True
|
|
|
|
def create_widget (self):
|
|
vbox = gtk.VBox (spacing=6)
|
|
self.label = gtk.Label ()
|
|
self.label.set_line_wrap (True)
|
|
self.progress = gtk.ProgressBar ()
|
|
self.progress.set_pulse_step (0.01)
|
|
vbox.pack_start (self.label, expand=False)
|
|
vbox.pack_start (self.progress, expand=False)
|
|
gobject.timeout_add (10, self.pulse)
|
|
return vbox
|
|
|
|
def set_label (self, text):
|
|
self.label.set_text (text)
|
|
|
|
def reset_label (self):
|
|
self.set_label ("This operation may take a while")
|
|
|
|
class ReportbugAssistant (gtk.Assistant):
|
|
def __init__ (self, application):
|
|
gtk.Assistant.__init__ (self)
|
|
self.set_title ('Reportbug')
|
|
self.application = application
|
|
self.showing_page = None
|
|
self.requested_page = None
|
|
self.progress_page = None
|
|
self.set_default_size (600, 400)
|
|
self.set_forward_page_func (self.forward)
|
|
self.connect_signals ()
|
|
self.setup_pages ()
|
|
|
|
def connect_signals (self):
|
|
self.connect ('cancel', self.close)
|
|
self.connect ('prepare', self.on_prepare)
|
|
self.connect ('delete-event', self.close)
|
|
self.connect ('apply', self.close)
|
|
|
|
def on_prepare (self, assistant, widget):
|
|
# If the user goes back then forward, we must ensure the feedback value to reportbug must be sent
|
|
# when the user clicks on "Forward" to the requested page by reportbug
|
|
if self.showing_page and self.showing_page == self.requested_page and self.get_current_page () > self.showing_page.page_num:
|
|
self.application.put_next_value ()
|
|
# Reportbug doesn't support going back, so make widgets insensitive
|
|
self.showing_page.widget.set_sensitive (False)
|
|
self.showing_page.switch_out ()
|
|
|
|
self.showing_page = widget.page
|
|
# Some pages might have changed the label in the while
|
|
if self.showing_page == self.progress_page:
|
|
self.progress_page.reset_label ()
|
|
|
|
self.showing_page.setup_focus ()
|
|
|
|
def close (self, *args):
|
|
sys.exit (0)
|
|
|
|
def forward (self, page_num):
|
|
return page_num + 1
|
|
|
|
def forward_page (self):
|
|
self.set_current_page (self.forward (self.showing_page.page_num))
|
|
|
|
def set_next_page (self, page):
|
|
self.requested_page = page
|
|
# If we're in progress immediately show this guy
|
|
if self.showing_page == self.progress_page:
|
|
self.set_current_page (page.page_num)
|
|
|
|
def set_progress_label (self, text, *args, **kwargs):
|
|
self.progress_page.set_label (text % args)
|
|
|
|
def setup_pages (self):
|
|
# We insert pages between the intro and the progress, so that we give the user the feedback
|
|
# that the applications is still running when he presses the "Forward" button
|
|
self.showing_page = IntroPage (self)
|
|
self.showing_page.switch_in ()
|
|
self.progress_page = ProgressPage (self)
|
|
self.progress_page.switch_in ()
|
|
self.set_current_page (0)
|
|
Page.next_page_num = 1
|
|
|
|
|
|
# Dialogs
|
|
|
|
|
|
class YesNoDialog (ReportbugConnector, gtk.MessageDialog):
|
|
def __init__ (self, application):
|
|
gtk.MessageDialog.__init__ (self, assistant, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
|
|
gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
|
|
self.application = application
|
|
self.connect ('response', self.on_response)
|
|
|
|
def on_response (self, dialog, res):
|
|
self.application.set_next_value (res == gtk.RESPONSE_YES)
|
|
self.application.put_next_value ()
|
|
self.destroy ()
|
|
|
|
def execute_operation (self, msg, yeshelp=None, nohelp=None, default=True, nowrap=False):
|
|
self.set_markup (msg+"?")
|
|
if default:
|
|
self.set_default_response (gtk.RESPONSE_YES)
|
|
else:
|
|
self.set_default_response (gtk.RESPONSE_NO)
|
|
self.show_all ()
|
|
|
|
class DisplayFailureDialog (ReportbugConnector, gtk.MessageDialog):
|
|
def __init__ (self, application):
|
|
gtk.MessageDialog.__init__ (self, assistant, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
|
|
gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE)
|
|
self.application = application
|
|
self.connect ('response', self.on_response)
|
|
|
|
def on_response (self, dialog, res):
|
|
self.application.put_next_value ()
|
|
self.destroy ()
|
|
|
|
def execute_operation (self, msg):
|
|
self.set_markup (msg)
|
|
self.show_all ()
|
|
|
|
class GetFilenameDialog (ReportbugConnector, gtk.FileChooserDialog):
|
|
def __init__ (self, application):
|
|
gtk.FileChooserDialog.__init__ (self, '', assistant, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
|
|
gtk.STOCK_OPEN, gtk.RESPONSE_OK))
|
|
self.application = application
|
|
self.connect ('response', self.on_response)
|
|
|
|
def on_response (self, dialog, res):
|
|
value = None
|
|
if res == gtk.RESPONSE_OK:
|
|
value = self.get_filename ()
|
|
|
|
self.application.set_next_value (value)
|
|
self.application.put_next_value ()
|
|
self.destroy ()
|
|
|
|
def execute_operation (self, title, force_prompt=False):
|
|
self.set_title (ask_free (title))
|
|
self.show_all ()
|
|
|
|
def log_message (*args, **kwargs):
|
|
return assistant.set_progress_label (*args, **kwargs)
|
|
|
|
def select_multiple (*args, **kwargs):
|
|
kwargs['multiple'] = True
|
|
kwargs['empty_ok'] = True
|
|
return menu (*args, **kwargs)
|
|
|
|
def get_multiline (prompt, *args, **kwargs):
|
|
if 'ENTER' in prompt:
|
|
# This is a list, let's handle it the best way
|
|
return get_list (prompt, *args, **kwargs)
|
|
else:
|
|
return get_multiline (prompt, *args, **kwargs)
|
|
|
|
pages = { 'get_string': GetStringPage,
|
|
'menu': MenuPage,
|
|
'handle_bts_query': HandleBTSQueryPage,
|
|
'long_message': LongMessagePage,
|
|
'display_report': DisplayReportPage,
|
|
'final_message': FinalMessagePage,
|
|
'spawn_editor': EditorPage,
|
|
'select_options': SelectOptionsPage,
|
|
'get_list': GetListPage }
|
|
dialogs = { 'yes_no': YesNoDialog,
|
|
'get_filename': GetFilenameDialog,
|
|
'display_failure': DisplayFailureDialog, }
|
|
|
|
def create_forwarder (parent, klass):
|
|
def func (*args, **kwargs):
|
|
op = klass (parent)
|
|
try:
|
|
args, kwargs = op.sync_pre_operation (*args, **kwargs)
|
|
except SyncReturn, e:
|
|
return e.result
|
|
application.run_once_in_main_thread (op.execute_operation, *args, **kwargs)
|
|
return application.get_last_value ()
|
|
return func
|
|
|
|
def forward_operations (parent, operations):
|
|
for operation, klass in operations.iteritems ():
|
|
globals()[operation] = create_forwarder (parent, klass)
|
|
|
|
def initialize ():
|
|
global application, assistant
|
|
# Exception hook
|
|
oldhook = sys.excepthook
|
|
sys.excepthook = ExceptionDialog.create_excepthook (oldhook)
|
|
|
|
# GTK settings
|
|
gtk.window_set_default_icon_from_file (DEBIAN_LOGO)
|
|
gtk.link_button_set_uri_hook (lambda button, uri: launch_browser (uri))
|
|
|
|
application = ReportbugApplication ()
|
|
assistant = ReportbugAssistant (application)
|
|
|
|
# Forwarders
|
|
forward_operations (assistant, pages)
|
|
forward_operations (application, dialogs)
|
|
|
|
application.start ()
|
|
|
|
def test ():
|
|
# Write some tests here
|
|
print select_options ('test', 'A', {'a': 'A test'})
|
|
print get_multiline ('ENTER', empty_ok=True)
|
|
print get_string ("test")
|
|
page = HandleBTSQueryPage (assistant)
|
|
application.run_once_in_main_thread (page.execute_operation, [('test', (Bug ('#123 [test] [we] we we Reported by: test;' ), Bug ('#123 [test] [we] we we Reported by: test;')))], 'test')
|
|
return application.get_last_value ()
|
|
|
|
if __name__ == '__main__':
|
|
initialize ()
|
|
test ()
|
|
|