# DistUpgradeViewKDE.py
#
#  Copyright (c) 2007 Canonical Ltd
#  Copyright (c) 2014-2018 Harald Sitter <sitter@kde.org>
#  Copyright (c) 2024 Simon Quigley <tsimonq2@ubuntu.com>
#  Copyright (c) 2026 Erich Eickmeyer <eeickmeyer@ubuntu.com>
#
#  Author: Jonathan Riddell <jriddell@ubuntu.com>
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License as
#  published by the Free Software Foundation; either version 2 of the
#  License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
#  USA

from .PyQtCompat import (
    uic, Qt, QLocale, QTranslator, QTimer, QEventLoop,
    QTextEdit, QApplication, QDialog, QMessageBox, QDialogButtonBox,
    QTreeWidgetItem, QPushButton, QWidget, QHBoxLayout, QLabel,
    QTextOption, QPixmap, QIcon, QTextCursor,
)

import sys
import locale
import logging
import time
import subprocess
import traceback

import apt
import apt_pkg
import distro_info
import shlex # for osrelease
import os

import pty

from .DistUpgradeApport import run_apport, apport_crash

from .DistUpgradeView import DistUpgradeView, FuzzyTimeToStr, InstallProgress, AcquireProgress
from .DistUpgradeConfigParser import DistUpgradeConfig
from .telemetry import get as get_telemetry

import select
import gettext
from .DistUpgradeGettext import gettext as _
from .DistUpgradeGettext import unicode_gettext

from .QUrlOpener import QUrlOpener

# FIXME: what's the purpose?
def utf8(s, errors="strict"):
    if isinstance(s, bytes):
        return s.decode("UTF-8", errors)
    else:
        return s

# FIXME: what's the purpose?
def loadUi(file, parent):
    if os.path.exists(file):
        uic.loadUi(file, parent)
    else:
        #FIXME find file
        print("error, can't find file: " + file)

def _find_pixmap(path):
    if os.path.exists(path):
        return QPixmap(path)
    return None

def _icon(name, fallbacks = []):
    return QIcon.fromTheme(name)

# QWidget adjustSize when run on a maximized window will make Qt 5.9, earlier,
# and probably also later, lose its state. Qt will think the window is no longer
# maximized, while in fact it is. This results in parts of the window no longer
# getting redrawn as the window manager will think it maximized but Qt thinks it
# is not and thus not send repaints for the regions it thinks do not exist.
# To prevent this from happening monkey patch adjustSize to not ever run on
# maximized windows.
def adjustSize(self):
    if not self.isMaximized():
        self.origAdjustSize(self)
QWidget.origAdjustSize = QWidget.adjustSize
QWidget.adjustSize = adjustSize

class _OSRelease:
    DEFAULT_OS_RELEASE_FILE = '/etc/os-release'
    OS_RELEASE_FILE = '/etc/os-release'

    def __init__(self, lsb_compat=True):
        self.result = {}
        self.valid = False
        self.file = _OSRelease.OS_RELEASE_FILE

        if not os.path.isfile(self.file):
            return

        self.parse()
        self.valid = True

        if lsb_compat:
            self.inject_lsb_compat()

    def inject_lsb_compat(self):
        self.result['Distributor ID'] = self.result['ID']
        self.result['Description'] = self.result['PRETTY_NAME']
        # Optionals as per os-release spec.
        self.result['Codename'] = self.result.get('VERSION_CODENAME')
        if not self.result['Codename']:
            # Transient Ubuntu 16.04 field (LP: #1598212)
            self.result['Codename'] = self.result.get('UBUNTU_CODENAME')
        self.result['Release'] = self.result.get('VERSION_ID')

    def parse(self):
        f = open(self.file, 'r')
        for line in f:
            line = line.strip()
            if not line:
                continue
            self.parse_entry(*line.split('=', 1))
        f.close()

    def parse_entry(self, key, value):
        value = self.parse_value(value) # Values can be shell strings...
        if key == "ID_LIKE" and isinstance(value, str):
            # ID_LIKE is specified as quoted space-separated list. This will
            # be parsed as string that we need to split manually.
            value = value.split(' ')
        self.result[key] = value

    def parse_value(self, value):
        values = shlex.split(value)
        if len(values) == 1:
            return values[0]
        return values


class DumbTerminal(QTextEdit):
    """ A very dumb terminal """
    def __init__(self, installProgress, parent_frame):
        " really dumb terminal with simple editing support "
        QTextEdit.__init__(self, "", parent_frame)
        self.installProgress = installProgress
        self.setFontFamily("Monospace")
        # FIXME: fixed font size set!!!
        self.setFontPointSize(8)
        self.setWordWrapMode(QTextOption.WrapMode.NoWrap)
        self.setUndoRedoEnabled(False)
        self.setOverwriteMode(True)
        self._block = False
        #self.connect(self, SIGNAL("cursorPositionChanged()"),
        #             self.onCursorPositionChanged)

    def fork(self):
        """pty voodoo"""
        (self.child_pid, self.installProgress.master_fd) = pty.fork()
        if self.child_pid == 0:
            os.environ["TERM"] = "dumb"
        return self.child_pid

    def update_interface(self):
        (rlist, wlist, xlist) = select.select([self.installProgress.master_fd],[],[], 0)
        if len(rlist) > 0:
            line = os.read(self.installProgress.master_fd, 255)
            self.insertWithTermCodes(utf8(line))
        QApplication.processEvents()

    def insertWithTermCodes(self, text):
        """ support basic terminal codes """
        # Move cursor to end before inserting so text is always appended
        self.moveCursor(QTextCursor.MoveOperation.End)
        display_text = ""
        for c in text:
            # \b - backspace - this seems to comes as "^H" now ??!
            if ord(c) == 8:
                self.insertPlainText(display_text)
                self.textCursor().deletePreviousChar()
                display_text=""
            # \r - is filtered out
            elif c == chr(13):
                pass
            # \a - bell - ignore for now
            elif c == chr(7):
                pass
            else:
                display_text += c
        self.insertPlainText(display_text)
        # Scroll to follow new output
        self.ensureCursorVisible()

    def keyPressEvent(self, ev):
        """ send (ascii) key events to the pty """
        # no master_fd yet
        if not hasattr(self.installProgress, "master_fd"):
            return
        # special handling for backspace
        if ev.key() == Qt.Key.Key_Backspace:
            #print("sent backspace")
            os.write(self.installProgress.master_fd, b'\x08')
            return
        # do nothing for events like "shift"
        if not ev.text():
            return
        # now send the key event to the terminal as utf-8
        os.write(self.installProgress.master_fd, ev.text().encode('utf-8'))

    def onCursorPositionChanged(self):
        """ helper that ensures that the cursor is always at the end """
        if self._block:
            return
        # block signals so that we do not run into a recursion
        self._block = True
        self.moveCursor(QTextCursor.MoveOperation.End)
        self._block = False


class KDEOpProgress(apt.progress.base.OpProgress):
  """ methods on the progress bar """
  def __init__(self, progressbar, progressbar_label):
      self.progressbar = progressbar
      self.progressbar_label = progressbar_label
      #self.progressbar.set_pulse_step(0.01)
      #self.progressbar.pulse()

  def update(self, percent=None):
      super(KDEOpProgress, self).update(percent)
      #if self.percent > 99:
      #    self.progressbar.set_fraction(1)
      #else:
      #    self.progressbar.pulse()
      #self.progressbar.set_fraction(self.percent/100.0)
      self.progressbar.setValue(int(self.percent))
      QApplication.processEvents()

  def done(self):
      self.progressbar_label.setText("")


class KDEAcquireProgressAdapter(AcquireProgress):
    """ methods for updating the progress bar while fetching packages """
    # FIXME: we really should have some sort of "we are at step"
    # xy in the gui
    # FIXME2: we need to thing about mediaCheck here too
    def __init__(self, parent):
        AcquireProgress.__init__(self)
        # if this is set to false the download will cancel
        self.status = parent.window_main.label_status
        self.progress = parent.window_main.progressbar_cache
        self.parent = parent

    def media_change(self, medium, drive):
      msg = _("Please insert '%s' into the drive '%s'") % (medium,drive)
      change = QMessageBox.question(self.parent.window_main, _("Media Change"), msg, QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.Cancel)
      if change == QMessageBox.StandardButton.Ok:
        return True
      return False

    def start(self):
        AcquireProgress.start(self)
        #self.progress.show()
        self.progress.setValue(0)
        self.status.show()

    def stop(self):
        self.parent.window_main.progress_text.setText("  ")
        self.status.setText(_("Fetching is complete"))

    def pulse(self, owner):
        """ we don't have a mainloop in this application, we just call processEvents here and elsewhere"""
        # FIXME: move the status_str and progress_str into python-apt
        # (python-apt need i18n first for this)
        AcquireProgress.pulse(self, owner)
        self.progress.setValue(int(self.percent))
        current_item = self.current_items + 1
        if current_item > self.total_items:
            current_item = self.total_items

        if self.current_cps > 0:
            current_cps = apt_pkg.size_to_str(self.current_cps)
            if isinstance(current_cps, bytes):
                current_cps = current_cps.decode(locale.getpreferredencoding())
            self.status.setText(_("Fetching file %li of %li at %sB/s") % (current_item, self.total_items, current_cps))
            self.parent.window_main.progress_text.setText("<i>" + _("About %s remaining") % FuzzyTimeToStr(self.eta) + "</i>")
        else:
            self.status.setText(_("Fetching file %li of %li") % (current_item, self.total_items))
            self.parent.window_main.progress_text.setText("  ")

        QApplication.processEvents()
        return True


class KDEInstallProgressAdapter(InstallProgress):
    """methods for updating the progress bar while installing packages"""
    # timeout with no status change when the terminal is expanded
    # automatically
    TIMEOUT_TERMINAL_ACTIVITY = 240

    def __init__(self,parent):
        InstallProgress.__init__(self)
        self._cache = None
        self.label_status = parent.window_main.label_status
        self.progress = parent.window_main.progressbar_cache
        self.progress_text = parent.window_main.progress_text
        self.parent = parent
        try:
            self._terminal_log = open("/var/log/dist-upgrade/term.log","wb")
        except Exception as e:
            # if something goes wrong (permission denied etc), use stdout
            logging.error("Can not open terminal log: '%s'" % e)
            if sys.version >= '3':
                self._terminal_log = sys.stdout.buffer
            else:
                self._terminal_log = sys.stdout
        # some options for dpkg to make it die less easily
        apt_pkg.config.set("DPkg::StopOnError","False")

    def start_update(self):
        InstallProgress.start_update(self)
        self.finished = False
        # FIXME: add support for the timeout
        # of the terminal (to display something useful then)
        # -> longer term, move this code into python-apt
        self.label_status.setText(_("Applying changes"))
        self.progress.setValue(0)
        self.progress_text.setText(" ")
        # do a bit of time-keeping
        self.start_time = 0.0
        self.time_ui = 0.0
        self.last_activity = 0.0
        self.parent.window_main.showTerminalButton.setEnabled(True)

    def error(self, pkg, errormsg):
        InstallProgress.error(self, pkg, errormsg)
        logging.error("got an error from dpkg for pkg: '%s': '%s'" % (pkg, errormsg))
        # we do not report followup errors from earlier failures
        if gettext.dgettext('dpkg', "dependency problems - leaving unconfigured") in errormsg:
          return False
        summary = _("Could not install '%s'") % pkg
        msg = _("The upgrade will continue but the '%s' package may not "
                "be in a working state. Please consider submitting a "
                "bug report about it.") % pkg
        msg = "<big><b>%s</b></big><br />%s" % (summary, msg)

        dialogue = QDialog(self.parent.window_main)
        loadUi("dialog_error.ui", dialogue)
        self.parent.translate_widget_children(dialogue)
        dialogue.label_error.setText(msg)
        if errormsg != None:
            dialogue.textview_error.setText(errormsg)
            dialogue.textview_error.show()
        else:
            dialogue.textview_error.hide()
        # Make sure we have a suitable size depending on whether or not the view is shown
        dialogue.adjustSize()
        dialogue.exec()

    def conffile(self, current, new):
        """ask question in case conffile has been changed by user"""
        logging.debug("got a conffile-prompt from dpkg for file: '%s'" % current)
        start = time.time()
        prim = _("Replace the customized configuration file\n'%s'?") % current
        sec = _("You will lose any changes you have made to this "
                "configuration file if you choose to replace it with "
                "a newer version.")
        markup = "<span weight=\"bold\" size=\"larger\">%s </span> \n\n%s" % (prim, sec)
        self.confDialogue = QDialog(self.parent.window_main)
        loadUi("dialog_conffile.ui", self.confDialogue)
        self.confDialogue.label_conffile.setText(markup)
        self.confDialogue.textview_conffile.hide()
        #FIXME, below to be tested
        #self.confDialogue.resize(self.confDialogue.minimumSizeHint())
        self.confDialogue.show_difference_button.clicked.connect(self.showConffile)

        # workaround silly dpkg
        if not os.path.exists(current):
          current = current+".dpkg-dist"

        # now get the diff
        if os.path.exists("/usr/bin/diff"):
          cmd = ["/usr/bin/diff", "-u", current, new]
          diff = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
          diff = diff.decode("UTF-8", "replace")
          self.confDialogue.textview_conffile.setText(diff)
        else:
          self.confDialogue.textview_conffile.setText(_("The 'diff' command was not found"))
        result = self.confDialogue.exec()
        self.time_ui += time.time() - start
        # if replace, send this to the terminal
        if result == QDialog.DialogCode.Accepted:
            os.write(self.master_fd, b"y\n")
        else:
            os.write(self.master_fd, b"n\n")

    def showConffile(self):
        if self.confDialogue.textview_conffile.isVisible():
            self.confDialogue.textview_conffile.hide()
            self.confDialogue.show_difference_button.setText(_("Show Difference >>>"))
        else:
            self.confDialogue.textview_conffile.show()
            self.confDialogue.show_difference_button.setText(_("<<< Hide Difference"))

    def fork(self):
        """pty voodoo"""
        (self.child_pid, self.master_fd) = pty.fork()
        if self.child_pid == 0:
            os.environ["TERM"] = "dumb"
            if ("DEBIAN_FRONTEND" not in os.environ or
                os.environ["DEBIAN_FRONTEND"] == "kde"):
                os.environ["DEBIAN_FRONTEND"] = "noninteractive"
            os.environ["APT_LISTCHANGES_FRONTEND"] = "none"
        logging.debug(" fork pid is: %s" % self.child_pid)
        return self.child_pid

    def status_change(self, pkg, percent, status):
        """update progress bar and label"""
        # start the timer when the first package changes its status
        if self.start_time == 0.0:
          #print("setting start time to %s" % self.start_time)
          self.start_time = time.time()
        self.progress.setValue(int(self.percent))
        self.label_status.setText(utf8(status.strip()))
        # start showing when we gathered some data
        if percent > 1.0:
          self.last_activity = time.time()
          self.activity_timeout_reported = False
          delta = self.last_activity - self.start_time
          # time wasted in conffile questions (or other ui activity)
          delta -= self.time_ui
          time_per_percent = (float(delta)/percent)
          eta = (100.0 - self.percent) * time_per_percent
          # only show if we have some sensible data (60sec < eta < 2days)
          if eta > 61.0 and eta < (60*60*24*2):
            self.progress_text.setText(_("About %s remaining") % FuzzyTimeToStr(eta))
          else:
            self.progress_text.setText(" ")

    def finish_update(self):
        self.label_status.setText("")

    def update_interface(self):
        """
        no mainloop in this application, just call processEvents lots here
        it's also important to sleep for a minimum amount of time
        """
        # log the output of dpkg (on the master_fd) to the terminal log
        while True:
            try:
                (rlist, wlist, xlist) = select.select([self.master_fd],[],[], 0)
                if len(rlist) > 0:
                    line = os.read(self.master_fd, 255)
                    self._terminal_log.write(line)
                    self.parent.terminal_text.insertWithTermCodes(
                        utf8(line, errors="replace"))
                else:
                    break
            except Exception as e:
                print(e)
                logging.debug("error reading from self.master_fd '%s'" % e)
                break

        # now update the GUI
        try:
          InstallProgress.update_interface(self)
        except ValueError as e:
          logging.error("got ValueError from InstallProgress.update_interface. Line was '%s' (%s)" % (self.read, e))
          # reset self.read so that it can continue reading and does not loop
          self.read = ""
        # check about terminal activity
        if self.last_activity > 0 and \
           (self.last_activity + self.TIMEOUT_TERMINAL_ACTIVITY) < time.time():
          if not self.activity_timeout_reported:
            #FIXME bug 95465, I can't recreate this, so here's a hacky fix
            try:
                logging.warning("no activity on terminal for %s seconds (%s)" % (self.TIMEOUT_TERMINAL_ACTIVITY, self.label_status.text()))
            except UnicodeEncodeError:
                logging.warning("no activity on terminal for %s seconds" % (self.TIMEOUT_TERMINAL_ACTIVITY))
            self.activity_timeout_reported = True
          self.parent.window_main.konsole_frame.show()
        QApplication.processEvents()
        time.sleep(0.02)

    def wait_child(self):
        while True:
            self.update_interface()
            (pid, res) = os.waitpid(self.child_pid,os.WNOHANG)
            if pid == self.child_pid:
                break
        # we need the full status here (not just WEXITSTATUS)
        return res


# inherit from the class created in window_main.ui
# to add the handler for closing the window
class UpgraderMainWindow(QWidget):

    def __init__(self):
        super().__init__()
        loadUi("window_main.ui", self)

    def setParent(self, parentRef):
        self.parent = parentRef

    def closeEvent(self, event):
        close = self.parent.on_window_main_delete_event()
        if close:
            event.accept()
        else:
            event.ignore()


class DistUpgradeViewKDE(DistUpgradeView):
    """KDE frontend of the distUpgrade tool"""
    def __init__(self, datadir=None, logdir=None):
        DistUpgradeView.__init__(self)

        get_telemetry().set_updater_type('KDE')
        # silence the PyQt6 logger
        logger = logging.getLogger("PyQt6")
        logger.setLevel(logging.INFO)
        if not datadir or datadir == '.':
          localedir=os.path.join(os.getcwd(),"mo")
          self.config = DistUpgradeConfig(os.getcwd())
        else:
          localedir="/usr/share/locale/ubuntu-release-upgrader"
          self.config = DistUpgradeConfig(datadir)

        # FIXME: i18n must be somewhere relative do this dir
        try:
          gettext.bindtextdomain("ubuntu-release-upgrader", localedir)
          gettext.textdomain("ubuntu-release-upgrader")
        except Exception as e:
          logging.warning("Error setting locales (%s)" % e)

        # we test for DISPLAY here, QApplication does not throw a
        # exception when run without DISPLAY but dies instead
        if not "DISPLAY" in os.environ:
            raise Exception("No DISPLAY in os.environ found")

        # Force environment to make sure Qt uses suitable theming and UX.
        os.environ["QT_PLATFORM_PLUGIN"] = "kde"
        # For above settings to apply automatically we need to indicate that we
        # are inside a full KDE session.
        os.environ["KDE_FULL_SESSION"] = "TRUE"
        # We also need to indicate version as otherwise KDElibs3 compatibility
        # might kick in such as in QIconLoader.cpp:QString fallbackTheme.
        os.environ["KDE_SESSION_VERSION"] = "6"
        # Pretty much all of the above but for Qt6
        os.environ["QT_QPA_PLATFORMTHEME"] = "kde"

        self.app = QApplication(["ubuntu-release-upgrader"])

        # Try to load default Qt translations so we don't have to worry about
        # QStandardButton translations.
        translator = QTranslator(self.app)
        translator.load(QLocale.system(), 'qt', '_', '/usr/share/qt6/translations')
        self.app.installTranslator(translator)

        QUrlOpener().setupUrlHandles()
        self.app.aboutToQuit.connect(QUrlOpener().teardownUrlHandles)

        messageIcon = _icon("system-software-update",
                            fallbacks=["/usr/share/icons/oxygen/48x48/apps/system-software-update.png",
                                       "/usr/share/icons/hicolor/48x48/apps/adept_manager.png"])
        self.app.setWindowIcon(messageIcon)
        self.window_main = UpgraderMainWindow()
        self.window_main.show()

        self.prev_step = None # keep a record of the latest step

        self._opCacheProgress = KDEOpProgress(self.window_main.progressbar_cache, self.window_main.progress_text)
        self._acquireProgress = KDEAcquireProgressAdapter(self)

        self._installProgress = KDEInstallProgressAdapter(self)

        # reasonable fault handler
        sys.excepthook = self._handleException

        self.window_main.showTerminalButton.setEnabled(False)
        self.window_main.showTerminalButton.clicked.connect(self.showTerminal)

        # init gettext
        gettext.bindtextdomain("ubuntu-release-upgrader",localedir)
        gettext.textdomain("ubuntu-release-upgrader")
        self.translate_widget_children()
        name = _OSRelease().result["PRETTY_NAME"]
        if not name or name == "Ubuntu":
            name = "Kubuntu"

        to_dist = self.config.get("Sources", "To")
        to_version = distro_info.UbuntuDistroInfo().version(to_dist) or 'devel'

        title_string = self.window_main.label_title.text()
        title_string = title_string.replace("Ubuntu", name)
        title_string = title_string.replace("%s", to_version)
        self.window_main.label_title.setText(title_string)

        # setup terminal text in hidden by default spot
        self.window_main.konsole_frame.hide()
        self.konsole_frame_layout = QHBoxLayout(self.window_main.konsole_frame)
        self.window_main.konsole_frame.setMinimumSize(600, 400)
        self.terminal_text = DumbTerminal(self._installProgress, self.window_main.konsole_frame)
        self.konsole_frame_layout.addWidget(self.terminal_text)
        self.terminal_text.show()

        # Run a local event loop briefly to ensure the window is fully
        # rendered. Using a local QEventLoop (instead of app.exec/app.exit)
        # avoids leaving the QApplication in a "quit" state, which in Qt6
        # can prevent subsequent processEvents() calls from processing
        # paint events — causing the window to appear black.
        loop = QEventLoop()
        QTimer.singleShot(100, loop.quit)
        loop.exec()

    def translate_widget_children(self, parentWidget=None):
        if parentWidget == None:
            parentWidget = self.window_main
        if isinstance(parentWidget, QDialog) or isinstance(parentWidget, QWidget):
            if str(parentWidget.windowTitle()) == "Error":
                parentWidget.setWindowTitle( gettext.dgettext("kdelibs", "Error"))
            else:
                parentWidget.setWindowTitle(_( str(parentWidget.windowTitle()) ))

        if parentWidget.children() != None:
            for widget in parentWidget.children():
                self.translate_widget(widget)
                self.translate_widget_children(widget)

    def translate_widget(self, widget):
        if isinstance(widget, QLabel) or isinstance(widget, QPushButton):
            if str(widget.text()) == "&Cancel":
                kdelibs = gettext.translation(
                    "kdelibs", gettext.textdomain("kdelibs"), fallback=True)
                widget.setText(unicode_gettext(kdelibs, "&Cancel"))
            elif str(widget.text()) == "&Close":
                kdelibs = gettext.translation(
                    "kdelibs", gettext.textdomain("kdelibs"), fallback=True)
                widget.setText(unicode_gettext(kdelibs, "&Close"))
            elif str(widget.text()) != "":
                widget.setText( _(str(widget.text())).replace("_", "&") )

    def _handleException(self, exctype, excvalue, exctb):
        """Crash handler."""

        if (issubclass(exctype, KeyboardInterrupt) or
            issubclass(exctype, SystemExit)):
            return

        # we handle the exception here, hand it to apport and run the
        # apport gui manually after it because we kill u-m during the upgrade
        # to prevent it from popping up for reboot notifications or FF restart
        # notifications or somesuch
        lines = traceback.format_exception(exctype, excvalue, exctb)
        logging.error("not handled exception in KDE frontend:\n%s" % "\n".join(lines))
        # we can't be sure that apport will run in the middle of a upgrade
        # so we still show a error message here
        apport_crash(exctype, excvalue, exctb)
        if not run_apport():
            tbtext = ''.join(traceback.format_exception(exctype, excvalue, exctb))
            dialog = QDialog(self.window_main)
            loadUi("dialog_error.ui", dialog)
            self.translate_widget_children(self.dialog)
            dialog.crash_detail.setText(tbtext)
            # Make sure we have a suitable size depending on whether or not the view is shown
            dialog.adjustSize()
            dialog.exec()
        sys.exit(1)

    def showTerminal(self):
        if self.window_main.konsole_frame.isVisible():
            self.window_main.konsole_frame.hide()
            self.window_main.showTerminalButton.setText(_("Show Terminal >>>"))
        else:
            self.window_main.konsole_frame.show()
            self.window_main.showTerminalButton.setText(_("<<< Hide Terminal"))
        self.window_main.adjustSize()

    def getAcquireProgress(self):
        return self._acquireProgress

    def getInstallProgress(self, cache):
        self._installProgress._cache = cache
        return self._installProgress

    def getOpCacheProgress(self):
        return self._opCacheProgress

    def update_status(self, msg):
        self.window_main.label_status.setText(msg)

    def hideStep(self, step):
        image = getattr(self.window_main,"image_step%i" % step.value)
        label = getattr(self.window_main,"label_step%i" % step.value)
        image.hide()
        label.hide()

    def abort(self):
        step = self.prev_step
        if step:
            image = getattr(self.window_main,"image_step%i" % step.value)
            cancelIcon = _icon("dialog-cancel",
                               fallbacks=["/usr/share/icons/oxygen/base/16x16/actions/dialog-cancel.png",
                                          "/usr/share/icons/crystalsvg/16x16/actions/cancel.png"])
            image.setPixmap(cancelIcon.pixmap(16, 16))
            image.show()

    def setStep(self, step):
        super(DistUpgradeViewKDE , self).setStep(step)
        okIcon = _icon("dialog-ok",
                       fallbacks=["/usr/share/icons/oxygen/base/16x16/actions/dialog-ok.png",
                                  "/usr/share/icons/crystalsvg/16x16/actions/ok.png"])
        arrowIcon = _icon("arrow-right",
                          fallbacks=["/usr/share/icons/oxygen/base/16x16/actions/arrow-right.png",
                                     "/usr/share/icons/crystalsvg/16x16/actions/1rightarrow.png"])

        if self.prev_step:
            image = getattr(self.window_main,"image_step%i" % self.prev_step.value)
            label = getattr(self.window_main,"label_step%i" % self.prev_step.value)
            image.setPixmap(okIcon.pixmap(16, 16))
            image.show()
            ##arrow.hide()
        self.prev_step = step
        # show the an arrow for the current step and make the label bold
        image = getattr(self.window_main,"image_step%i" % step.value)
        label = getattr(self.window_main,"label_step%i" % step.value)
        image.setPixmap(arrowIcon.pixmap(16, 16))
        image.show()
        label.setText("<b>" + label.text() + "</b>")

    def information(self, summary, msg, extended_msg=None):
        msg = "<big><b>%s</b></big><br />%s" % (summary,msg)

        dialogue = QDialog(self.window_main)
        loadUi("dialog_error.ui", dialogue)
        self.translate_widget_children(dialogue)
        dialogue.label_error.setText(msg)
        if extended_msg != None:
            dialogue.textview_error.setText(extended_msg)
            dialogue.textview_error.show()
        else:
            dialogue.textview_error.hide()
        dialogue.setWindowTitle(_("Information"))

        messageIcon = _icon("dialog-information",
                            fallbacks=["/usr/share/icons/oxygen/base/48x48/status/dialog-information.png",
                                       "/usr/share/icons/crystalsvg/32x32/actions/messagebox_info.png"])
        dialogue.image.setPixmap(messageIcon.pixmap(48, 48))
        # Make sure we have a suitable size depending on whether or not the view is shown
        dialogue.adjustSize()
        dialogue.exec()

    def error(self, summary, msg, extended_msg=None):
        msg="<big><b>%s</b></big><br />%s" % (summary, msg)

        dialogue = QDialog(self.window_main)
        loadUi("dialog_error.ui", dialogue)
        self.translate_widget_children(dialogue)
        dialogue.label_error.setText(msg)
        if extended_msg != None:
            dialogue.textview_error.setText(extended_msg)
            dialogue.textview_error.show()
        else:
            dialogue.textview_error.hide()

        messageIcon = _icon("dialog-error",
                            fallbacks=["/usr/share/icons/oxygen/base/48x48/status/dialog-error.png",
                                       "/usr/share/icons/crystalsvg/32x32/actions/messagebox_critical.png"])
        dialogue.image.setPixmap(messageIcon.pixmap(48, 48))
        # Make sure we have a suitable size depending on whether or not the view is shown
        dialogue.adjustSize()
        dialogue.exec()

        return False

    def confirmChanges(self, summary, changes, downloadSize,
                       actions=None, removal_bold=True):
        """show the changes dialogue"""
        # FIXME: add an allow list here for packages that we expect to be
        # removed (how to calc this automatically?)
        DistUpgradeView.confirmChanges(self, summary, changes,
                                       downloadSize)
        self.changesDialogue = QDialog(self.window_main)
        loadUi("dialog_changes.ui", self.changesDialogue)

        self.changesDialogue.treeview_details.hide()
        self.changesDialogue.buttonBox.helpRequested.connect(self.showChangesDialogueDetails)
        self.translate_widget_children(self.changesDialogue)
        self.changesDialogue.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText(_("&Start Upgrade"))
        self.changesDialogue.buttonBox.button(QDialogButtonBox.StandardButton.Help).setIcon(QIcon())
        self.changesDialogue.buttonBox.button(QDialogButtonBox.StandardButton.Help).setText(_("Details") + " >>>")

        messageIcon = _icon("dialog-warning",
                            fallbacks=["/usr/share/icons/oxygen/base/48x48/status/dialog-warning.png",
                                       "/usr/share/icons/crystalsvg/32x32/actions/messagebox_warning.png"])
        self.changesDialogue.question_pixmap.setPixmap(messageIcon.pixmap(48, 48))

        if actions != None:
            cancel = actions[0].replace("_", "")
            self.changesDialogue.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText(cancel)
            confirm = actions[1].replace("_", "")
            self.changesDialogue.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText(confirm)

        summaryText = "<big><b>%s</b></big>" % summary
        self.changesDialogue.label_summary.setText(summaryText)
        self.changesDialogue.label_changes.setText(self.confirmChangesMessage)
        # fill in the details
        self.changesDialogue.treeview_details.clear()
        self.changesDialogue.treeview_details.setHeaderLabels(["Packages"])
        self.changesDialogue.treeview_details.header().hide()
        for rm in self.toRemove:
            self.changesDialogue.treeview_details.insertTopLevelItem(0, QTreeWidgetItem(self.changesDialogue.treeview_details, [_("Remove %s") % rm.name]) )
        for rm in self.toRemoveAuto:
            self.changesDialogue.treeview_details.insertTopLevelItem(0, QTreeWidgetItem(self.changesDialogue.treeview_details, [_("Remove (was auto installed) %s") % rm.name]) )
        for inst in self.toInstall:
            self.changesDialogue.treeview_details.insertTopLevelItem(0, QTreeWidgetItem(self.changesDialogue.treeview_details, [_("Install %s") % inst.name]) )
        for up in self.toUpgrade:
            self.changesDialogue.treeview_details.insertTopLevelItem(0, QTreeWidgetItem(self.changesDialogue.treeview_details, [_("Upgrade %s") % up.name]) )

        # Use a suitable size for the window given the current content.
        self.changesDialogue.adjustSize()

        #FIXME resize label, stop it being shrinkable
        res = self.changesDialogue.exec()
        if res == QDialog.DialogCode.Accepted:
            return True
        return False

    def showChangesDialogueDetails(self):
        if self.changesDialogue.treeview_details.isVisible():
            self.changesDialogue.treeview_details.hide()
            self.changesDialogue.buttonBox.button(QDialogButtonBox.StandardButton.Help).setText(_("Details") + " >>>")
        else:
            self.changesDialogue.treeview_details.show()
            self.changesDialogue.buttonBox.button(QDialogButtonBox.StandardButton.Help).setText("<<< " + _("Details"))
        self.changesDialogue.adjustSize()

    def askYesNoQuestion(self, summary, msg, default='No'):
        answer = QMessageBox.question(self.window_main, summary, "<font>" + msg, QMessageBox.StandardButton.Yes|QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
        if answer == QMessageBox.StandardButton.Yes:
            return True
        return False

    def askCancelContinueQuestion(self, summary, msg, default='Cancel'):
        messageBox = QMessageBox(QMessageBox.Icon.Warning, summary, msg, QMessageBox.StandardButton.NoButton, self.window_main)
        continueButton = messageBox.addButton(QMessageBox.StandardButton.Apply)
        cancelButton = messageBox.addButton(QMessageBox.StandardButton.Cancel)
        continueButton.setText(_("Continue"))

        if default == 'Cancel':
            messageBox.setDefaultButton(cancelButton)
        else:
            messageBox.setDefaultButton(continueButton)

        if summary is None:
            flags = messageBox.windowFlags()
            messageBox.setWindowFlags(flags | Qt.WindowType.FramelessWindowHint)

        answer = messageBox.exec()
        if answer == QMessageBox.StandardButton.Apply:
            return True
        return False

    def confirmRestart(self):
        messageBox = QMessageBox(QMessageBox.Icon.Question, _("Restart required"), _("<b><big>Restart the system to complete the upgrade</big></b>"), QMessageBox.StandardButton.NoButton, self.window_main)
        yesButton = messageBox.addButton(QMessageBox.StandardButton.Yes)
        noButton = messageBox.addButton(QMessageBox.StandardButton.No)
        yesButton.setText(_("_Restart Now").replace("_", "&"))
        noButton.setText(gettext.dgettext("kdelibs", "&Close"))
        answer = messageBox.exec()
        if answer == QMessageBox.StandardButton.Yes:
            return True
        return False

    def processEvents(self):
        QApplication.processEvents()

    def pulseProgress(self, finished=False):
        # FIXME: currently we do nothing here because this is
        # run in a different python thread and QT explodes if the UI is
        # touched from a non QThread
        pass

    def on_window_main_delete_event(self):
        #FIXME make this user friendly
        text = _("""<b><big>Cancel the running upgrade?</big></b>

The system could be in an unusable state if you cancel the upgrade. You are strongly advised to resume the upgrade.""")
        text = text.replace("\n", "<br />")
        cancel = QMessageBox.warning(self.window_main, _("Cancel Upgrade?"), text, QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.No)
        if cancel == QMessageBox.StandardButton.Yes:
            return True
        return False
