qvm-template: Port initial code to PyQt.
This commit is contained in:
		
							parent
							
								
									79b6d8f72c
								
							
						
					
					
						commit
						7e8ee7e8cc
					
				@ -1,19 +1,19 @@
 | 
				
			|||||||
 | 
					import asyncio
 | 
				
			||||||
import collections
 | 
					import collections
 | 
				
			||||||
import concurrent
 | 
					 | 
				
			||||||
import concurrent.futures
 | 
					 | 
				
			||||||
import itertools
 | 
					import itertools
 | 
				
			||||||
import json
 | 
					import json
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import subprocess
 | 
					 | 
				
			||||||
import typing
 | 
					import typing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import gi
 | 
					import PyQt5
 | 
				
			||||||
gi.require_version('Gtk', '3.0')
 | 
					import PyQt5.QtWidgets
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#pylint: disable=wrong-import-position
 | 
					from . import ui_qvmtemplate
 | 
				
			||||||
from gi.repository import GLib
 | 
					from . import ui_templateinstallconfirmdlg
 | 
				
			||||||
from gi.repository import Gtk
 | 
					from . import ui_templateinstallprogressdlg
 | 
				
			||||||
from gi.repository import Pango
 | 
					from . import utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#pylint: disable=invalid-name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
BASE_CMD = ['qvm-template', '--enablerepo=*', '--yes', '--quiet']
 | 
					BASE_CMD = ['qvm-template', '--enablerepo=*', '--yes', '--quiet']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -28,16 +28,10 @@ class Template(typing.NamedTuple):
 | 
				
			|||||||
    licence: str
 | 
					    licence: str
 | 
				
			||||||
    url: str
 | 
					    url: str
 | 
				
			||||||
    summary: str
 | 
					    summary: str
 | 
				
			||||||
    # --- internal ---
 | 
					    # ---- internal ----
 | 
				
			||||||
    description: str
 | 
					    description: str
 | 
				
			||||||
    default_status: str
 | 
					    default_status: str
 | 
				
			||||||
    weight: int
 | 
					    # ------------------
 | 
				
			||||||
    model: Gtk.TreeModel
 | 
					 | 
				
			||||||
    # ----------------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # XXX: Is there a better way of doing this?
 | 
					 | 
				
			||||||
    TYPES = [str, str, str, str, int, str, str, str,
 | 
					 | 
				
			||||||
        str, str, str, str, int, Gtk.TreeModel]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    COL_NAMES = [
 | 
					    COL_NAMES = [
 | 
				
			||||||
        'Status',
 | 
					        'Status',
 | 
				
			||||||
@ -49,27 +43,24 @@ class Template(typing.NamedTuple):
 | 
				
			|||||||
        'Install Time',
 | 
					        'Install Time',
 | 
				
			||||||
        'License',
 | 
					        'License',
 | 
				
			||||||
        'URL',
 | 
					        'URL',
 | 
				
			||||||
        'Summary']
 | 
					        'Summary'
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def build(status, entry, model):
 | 
					    def build(status, entry):
 | 
				
			||||||
        return Template(
 | 
					        return Template(
 | 
				
			||||||
            status,
 | 
					            status,
 | 
				
			||||||
            entry['name'],
 | 
					            entry['name'],
 | 
				
			||||||
            '%s:%s-%s' % (entry['epoch'], entry['version'], entry['release']),
 | 
					            '%s:%s-%s' % (entry['epoch'], entry['version'], entry['release']),
 | 
				
			||||||
            entry['reponame'],
 | 
					            entry['reponame'],
 | 
				
			||||||
            # XXX: This may overflow glib ints, though pretty unlikely in the
 | 
					            int(entry['size']) // 1000,
 | 
				
			||||||
            #       foreseeable future
 | 
					 | 
				
			||||||
            int(entry['size']) / 1000,
 | 
					 | 
				
			||||||
            entry['buildtime'],
 | 
					            entry['buildtime'],
 | 
				
			||||||
            entry['installtime'],
 | 
					            entry['installtime'],
 | 
				
			||||||
            entry['license'],
 | 
					            entry['license'],
 | 
				
			||||||
            entry['url'],
 | 
					            entry['url'],
 | 
				
			||||||
            entry['summary'],
 | 
					            entry['summary'],
 | 
				
			||||||
            entry['description'],
 | 
					            entry['description'],
 | 
				
			||||||
            status,
 | 
					            status
 | 
				
			||||||
            Pango.Weight.BOOK,
 | 
					 | 
				
			||||||
            model
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Action(typing.NamedTuple):
 | 
					class Action(typing.NamedTuple):
 | 
				
			||||||
@ -80,88 +71,7 @@ class Action(typing.NamedTuple):
 | 
				
			|||||||
    TYPES = [str, str, str]
 | 
					    TYPES = [str, str, str]
 | 
				
			||||||
    COL_NAMES = ['Operation', 'Name', 'Version']
 | 
					    COL_NAMES = ['Operation', 'Name', 'Version']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# TODO: Set default window sizes
 | 
					class TemplateStatusDelegate(PyQt5.QtWidgets.QStyledItemDelegate):
 | 
				
			||||||
 | 
					 | 
				
			||||||
class ConfirmDialog(Gtk.Dialog):
 | 
					 | 
				
			||||||
    def __init__(self, parent, actions):
 | 
					 | 
				
			||||||
        super(ConfirmDialog, self).__init__(
 | 
					 | 
				
			||||||
            title='Confirmation', transient_for=parent, modal=True)
 | 
					 | 
				
			||||||
        self.add_buttons(
 | 
					 | 
				
			||||||
            Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
 | 
					 | 
				
			||||||
            Gtk.STOCK_OK, Gtk.ResponseType.OK)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        box = self.get_content_area()
 | 
					 | 
				
			||||||
        self.msg = Gtk.Label()
 | 
					 | 
				
			||||||
        self.msg.set_markup((
 | 
					 | 
				
			||||||
            '<b>WARNING: Local changes made to the following'
 | 
					 | 
				
			||||||
            ' templates will be overwritten! Continue?</b>'))
 | 
					 | 
				
			||||||
        box.add(self.msg)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.store = Gtk.ListStore(*Action.TYPES)
 | 
					 | 
				
			||||||
        self.listing = Gtk.TreeView(model=self.store)
 | 
					 | 
				
			||||||
        for idx, colname in enumerate(Action.COL_NAMES):
 | 
					 | 
				
			||||||
            renderer = Gtk.CellRendererText()
 | 
					 | 
				
			||||||
            col = Gtk.TreeViewColumn(colname, renderer, text=idx)
 | 
					 | 
				
			||||||
            self.listing.append_column(col)
 | 
					 | 
				
			||||||
            col.set_sort_column_id(idx)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for row in actions:
 | 
					 | 
				
			||||||
            self.store.append(row)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.scrollable_listing = Gtk.ScrolledWindow()
 | 
					 | 
				
			||||||
        self.scrollable_listing.add(self.listing)
 | 
					 | 
				
			||||||
        box.pack_start(self.scrollable_listing, True, True, 16)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.show_all()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ProgressDialog(Gtk.Dialog):
 | 
					 | 
				
			||||||
    def __init__(self, parent):
 | 
					 | 
				
			||||||
        super(ProgressDialog, self).__init__(
 | 
					 | 
				
			||||||
            title='Processing...', transient_for=parent, modal=True)
 | 
					 | 
				
			||||||
        box = self.get_content_area()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.spinner = Gtk.Spinner()
 | 
					 | 
				
			||||||
        self.spinner.start()
 | 
					 | 
				
			||||||
        box.add(self.spinner)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.msg = Gtk.Label()
 | 
					 | 
				
			||||||
        self.msg.set_text('Processing...')
 | 
					 | 
				
			||||||
        box.add(self.msg)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.infobox = Gtk.TextView()
 | 
					 | 
				
			||||||
        self.scrollable = Gtk.ScrolledWindow()
 | 
					 | 
				
			||||||
        self.scrollable.add(self.infobox)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        box.pack_start(self.scrollable, True, True, 16)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.show_all()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def finish(self, success):
 | 
					 | 
				
			||||||
        self.spinner.stop()
 | 
					 | 
				
			||||||
        if success:
 | 
					 | 
				
			||||||
            self.msg.set_text('Operations succeeded.')
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            self.msg.set_markup('<b>Error:</b>')
 | 
					 | 
				
			||||||
        self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
 | 
					 | 
				
			||||||
        self.run()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class QubesTemplateApp(Gtk.Window):
 | 
					 | 
				
			||||||
    def __init__(self):
 | 
					 | 
				
			||||||
        super(QubesTemplateApp, self).__init__(title='Qubes Template Manager')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.iconsize = Gtk.IconSize.SMALL_TOOLBAR
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.executor = concurrent.futures.ThreadPoolExecutor()
 | 
					 | 
				
			||||||
        self.outerbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
 | 
					 | 
				
			||||||
        self.__build_action_models()
 | 
					 | 
				
			||||||
        self.__build_toolbar()
 | 
					 | 
				
			||||||
        self.__build_listing()
 | 
					 | 
				
			||||||
        self.__build_infobox()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.add(self.outerbox)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __build_action_models(self):
 | 
					 | 
				
			||||||
        #pylint: disable=invalid-name
 | 
					 | 
				
			||||||
    OPS = [
 | 
					    OPS = [
 | 
				
			||||||
        ['Installed', 'Reinstall', 'Remove'],
 | 
					        ['Installed', 'Reinstall', 'Remove'],
 | 
				
			||||||
        ['Extra', 'Remove'],
 | 
					        ['Extra', 'Remove'],
 | 
				
			||||||
@ -169,96 +79,162 @@ class QubesTemplateApp(Gtk.Window):
 | 
				
			|||||||
        ['Downgradable', 'Downgrade', 'Remove'],
 | 
					        ['Downgradable', 'Downgrade', 'Remove'],
 | 
				
			||||||
        ['Available', 'Install']
 | 
					        ['Available', 'Install']
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
        self.action_models = {}
 | 
					 | 
				
			||||||
        for ops in OPS:
 | 
					 | 
				
			||||||
            # First element is the default status for the certain class of
 | 
					 | 
				
			||||||
            # templates
 | 
					 | 
				
			||||||
            self.action_models[ops[0]] = Gtk.ListStore(str)
 | 
					 | 
				
			||||||
            for oper in ops:
 | 
					 | 
				
			||||||
                self.action_models[ops[0]].append([oper])
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __build_toolbar(self):
 | 
					    def createEditor(self, parent, option, index):
 | 
				
			||||||
        self.toolbar = Gtk.Toolbar()
 | 
					        _ = option # unused
 | 
				
			||||||
        self.btn_refresh = Gtk.ToolButton(
 | 
					        editor = PyQt5.QtWidgets.QComboBox(parent)
 | 
				
			||||||
            icon_widget=Gtk.Image.new_from_icon_name(
 | 
					        # Otherwise the internalPointer can be overwritten with a QComboBox
 | 
				
			||||||
                'view-refresh', self.iconsize),
 | 
					        index = index.model().index(index.row(), index.column())
 | 
				
			||||||
            label='Refresh')
 | 
					        kind = index.internalPointer().default_status
 | 
				
			||||||
        self.btn_refresh.connect('clicked', self.refresh)
 | 
					        for op_list in TemplateStatusDelegate.OPS:
 | 
				
			||||||
        self.toolbar.insert(self.btn_refresh, 0)
 | 
					            if op_list[0] == kind:
 | 
				
			||||||
 | 
					                for op in op_list:
 | 
				
			||||||
 | 
					                    editor.addItem(op)
 | 
				
			||||||
 | 
					                editor.currentIndexChanged.connect(self.currentIndexChanged)
 | 
				
			||||||
 | 
					                editor.showPopup()
 | 
				
			||||||
 | 
					                return editor
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.btn_install = Gtk.ToolButton(
 | 
					    def setEditorData(self, editor, index):
 | 
				
			||||||
            icon_widget=Gtk.Image.new_from_icon_name('go-down', self.iconsize),
 | 
					        #pylint: disable=no-self-use
 | 
				
			||||||
            label='Apply')
 | 
					        cur = index.data()
 | 
				
			||||||
        self.btn_install.connect('clicked', self.show_confirm)
 | 
					        idx = editor.findText(cur)
 | 
				
			||||||
        self.toolbar.insert(self.btn_install, 1)
 | 
					        if idx >= 0:
 | 
				
			||||||
 | 
					            editor.setCurrentIndex(idx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.outerbox.pack_start(self.toolbar, False, True, 0)
 | 
					    def setModelData(self, editor, model, index):
 | 
				
			||||||
 | 
					        #pylint: disable=no-self-use
 | 
				
			||||||
 | 
					        model.setData(index, editor.currentText())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __build_listing(self):
 | 
					    def updateEditorGeometry(self, editor, option, index):
 | 
				
			||||||
        self.store = Gtk.ListStore(*Template.TYPES)
 | 
					        #pylint: disable=no-self-use
 | 
				
			||||||
 | 
					        _ = index # unused
 | 
				
			||||||
 | 
					        editor.setGeometry(option.rect)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.listing = Gtk.TreeView(model=self.store)
 | 
					    @PyQt5.QtCore.pyqtSlot()
 | 
				
			||||||
        self.cols = []
 | 
					    def currentIndexChanged(self):
 | 
				
			||||||
        for idx, colname in enumerate(Template.COL_NAMES):
 | 
					        self.commitData.emit(self.sender())
 | 
				
			||||||
            if colname == 'Status':
 | 
					 | 
				
			||||||
                renderer = Gtk.CellRendererCombo()
 | 
					 | 
				
			||||||
                renderer.set_property('editable', True)
 | 
					 | 
				
			||||||
                renderer.set_property('has-entry', False)
 | 
					 | 
				
			||||||
                renderer.set_property('text-column', 0)
 | 
					 | 
				
			||||||
                renderer.connect('edited', self.entry_edit)
 | 
					 | 
				
			||||||
                col = Gtk.TreeViewColumn(
 | 
					 | 
				
			||||||
                    colname,
 | 
					 | 
				
			||||||
                    renderer,
 | 
					 | 
				
			||||||
                    text=idx,
 | 
					 | 
				
			||||||
                    weight=len(Template.TYPES) - 2,
 | 
					 | 
				
			||||||
                    model=len(Template.TYPES) - 1)
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                renderer = Gtk.CellRendererText()
 | 
					 | 
				
			||||||
                col = Gtk.TreeViewColumn(
 | 
					 | 
				
			||||||
                    colname,
 | 
					 | 
				
			||||||
                    renderer,
 | 
					 | 
				
			||||||
                    text=idx,
 | 
					 | 
				
			||||||
                    weight=len(Template.TYPES) - 2)
 | 
					 | 
				
			||||||
            # Right-align for integers
 | 
					 | 
				
			||||||
            if Template.TYPES[idx] is int:
 | 
					 | 
				
			||||||
                renderer.set_property('xalign', 1.0)
 | 
					 | 
				
			||||||
            self.cols.append(col)
 | 
					 | 
				
			||||||
            self.listing.append_column(col)
 | 
					 | 
				
			||||||
            col.set_sort_column_id(idx)
 | 
					 | 
				
			||||||
        sel = self.listing.get_selection()
 | 
					 | 
				
			||||||
        sel.set_mode(Gtk.SelectionMode.MULTIPLE)
 | 
					 | 
				
			||||||
        sel.connect('changed', self.update_info)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.scrollable_listing = Gtk.ScrolledWindow()
 | 
					class TemplateModel(PyQt5.QtCore.QAbstractItemModel):
 | 
				
			||||||
        self.scrollable_listing.add(self.listing)
 | 
					    def __init__(self):
 | 
				
			||||||
        self.scrollable_listing.set_visible(False)
 | 
					        super().__init__()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.spinner = Gtk.Spinner()
 | 
					        self.children = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.outerbox.pack_start(self.scrollable_listing, True, True, 0)
 | 
					    def flags(self, index):
 | 
				
			||||||
        self.outerbox.pack_start(self.spinner, True, True, 0)
 | 
					        if index.isValid() and index.column() == 0:
 | 
				
			||||||
 | 
					            return super().flags(index) | PyQt5.QtCore.Qt.ItemIsEditable
 | 
				
			||||||
 | 
					        return super().flags(index)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __build_infobox(self):
 | 
					    def sort(self, idx, order):
 | 
				
			||||||
        self.infobox = Gtk.TextView()
 | 
					        rev = (order == PyQt5.QtCore.Qt.AscendingOrder)
 | 
				
			||||||
        self.outerbox.pack_start(self.infobox, True, True, 16)
 | 
					        self.children.sort(key=lambda x: x[idx], reverse=rev)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def refresh(self, button=None):
 | 
					        self.dataChanged.emit(*self.row_index(0, self.rowCount() - 1))
 | 
				
			||||||
        # Ignore if we're already doing a refresh
 | 
					
 | 
				
			||||||
        #pylint: disable=no-member
 | 
					    def index(self, row, column, parent=PyQt5.QtCore.QModelIndex()):
 | 
				
			||||||
        if self.spinner.props.active:
 | 
					        if not self.hasIndex(row, column, parent):
 | 
				
			||||||
            return
 | 
					            return PyQt5.QtCore.QModelIndex()
 | 
				
			||||||
        self.scrollable_listing.set_visible(False)
 | 
					
 | 
				
			||||||
        self.spinner.start()
 | 
					        return self.createIndex(row, column, self.children[row])
 | 
				
			||||||
        self.spinner.set_visible(True)
 | 
					
 | 
				
			||||||
        self.store.clear()
 | 
					    def parent(self, child):
 | 
				
			||||||
        def worker():
 | 
					        #pylint: disable=no-self-use
 | 
				
			||||||
 | 
					        _ = child # unused
 | 
				
			||||||
 | 
					        return PyQt5.QtCore.QModelIndex()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def rowCount(self, parent=PyQt5.QtCore.QModelIndex()):
 | 
				
			||||||
 | 
					        #pylint: disable=no-self-use
 | 
				
			||||||
 | 
					        _ = parent # unused
 | 
				
			||||||
 | 
					        return len(self.children)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def columnCount(self, parent=PyQt5.QtCore.QModelIndex()):
 | 
				
			||||||
 | 
					        #pylint: disable=no-self-use
 | 
				
			||||||
 | 
					        _ = parent # unused
 | 
				
			||||||
 | 
					        return len(Template.COL_NAMES)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def hasChildren(self, index=PyQt5.QtCore.QModelIndex()):
 | 
				
			||||||
 | 
					        #pylint: disable=no-self-use
 | 
				
			||||||
 | 
					        return index == PyQt5.QtCore.QModelIndex()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def data(self, index, role=PyQt5.QtCore.Qt.DisplayRole):
 | 
				
			||||||
 | 
					        if index.isValid():
 | 
				
			||||||
 | 
					            if role == PyQt5.QtCore.Qt.DisplayRole:
 | 
				
			||||||
 | 
					                return self.children[index.row()][index.column()]
 | 
				
			||||||
 | 
					            if role == PyQt5.QtCore.Qt.FontRole:
 | 
				
			||||||
 | 
					                font = PyQt5.QtGui.QFont()
 | 
				
			||||||
 | 
					                tpl = self.children[index.row()]
 | 
				
			||||||
 | 
					                font.setBold(tpl.status != tpl.default_status)
 | 
				
			||||||
 | 
					                return font
 | 
				
			||||||
 | 
					            if role == PyQt5.QtCore.Qt.TextAlignmentRole:
 | 
				
			||||||
 | 
					                if isinstance(self.children[index.row()][index.column()], int):
 | 
				
			||||||
 | 
					                    return PyQt5.QtCore.Qt.AlignRight
 | 
				
			||||||
 | 
					                return PyQt5.QtCore.Qt.AlignLeft
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setData(self, index, value, role=PyQt5.QtCore.Qt.EditRole):
 | 
				
			||||||
 | 
					        if index.isValid() and role == PyQt5.QtCore.Qt.EditRole:
 | 
				
			||||||
 | 
					            old_list = list(self.children[index.row()])
 | 
				
			||||||
 | 
					            old_list[index.column()] = value
 | 
				
			||||||
 | 
					            new_tpl = Template(*old_list)
 | 
				
			||||||
 | 
					            self.children[index.row()] = new_tpl
 | 
				
			||||||
 | 
					            self.dataChanged.emit(index, index)
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def headerData(self, section, orientation,
 | 
				
			||||||
 | 
					            role=PyQt5.QtCore.Qt.DisplayRole):
 | 
				
			||||||
 | 
					        #pylint: disable=no-self-use
 | 
				
			||||||
 | 
					        if section < len(Template.COL_NAMES) \
 | 
				
			||||||
 | 
					                and orientation == PyQt5.QtCore.Qt.Horizontal \
 | 
				
			||||||
 | 
					                and role == PyQt5.QtCore.Qt.DisplayRole:
 | 
				
			||||||
 | 
					            return Template.COL_NAMES[section]
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def removeRows(self, row, count, parent=PyQt5.QtCore.QModelIndex()):
 | 
				
			||||||
 | 
					        _ = parent # unused
 | 
				
			||||||
 | 
					        self.beginRemoveRows(PyQt5.QtCore.QModelIndex(), row, row + count)
 | 
				
			||||||
 | 
					        del self.children[row:row+count]
 | 
				
			||||||
 | 
					        self.endRemoveRows()
 | 
				
			||||||
 | 
					        self.dataChanged.emit(*self.row_index(row, row + count))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def row_index(self, low, high):
 | 
				
			||||||
 | 
					        return self.createIndex(low, 0), \
 | 
				
			||||||
 | 
					            self.createIndex(high, self.columnCount())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def set_templates(self, templates):
 | 
				
			||||||
 | 
					        self.removeRows(0, self.rowCount())
 | 
				
			||||||
 | 
					        cnt = sum(len(g) for _, g in templates.items())
 | 
				
			||||||
 | 
					        self.beginInsertRows(PyQt5.QtCore.QModelIndex(), 0, cnt - 1)
 | 
				
			||||||
 | 
					        for status, grp in templates.items():
 | 
				
			||||||
 | 
					            for tpl in grp:
 | 
				
			||||||
 | 
					                self.children.append(Template.build(status, tpl))
 | 
				
			||||||
 | 
					        self.endInsertRows()
 | 
				
			||||||
 | 
					        self.dataChanged.emit(*self.row_index(0, self.rowCount() - 1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_actions(self):
 | 
				
			||||||
 | 
					        actions = []
 | 
				
			||||||
 | 
					        for tpl in self.children:
 | 
				
			||||||
 | 
					            if tpl.status != tpl.default_status:
 | 
				
			||||||
 | 
					                actions.append(Action(tpl.status, tpl.name, tpl.evr))
 | 
				
			||||||
 | 
					        return actions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def refresh(self, refresh=True):
 | 
				
			||||||
        cmd = BASE_CMD[:]
 | 
					        cmd = BASE_CMD[:]
 | 
				
			||||||
            if button is not None:
 | 
					        if refresh:
 | 
				
			||||||
            # Force refresh if triggered by button press
 | 
					            # Force refresh if triggered by button press
 | 
				
			||||||
            cmd.append('--refresh')
 | 
					            cmd.append('--refresh')
 | 
				
			||||||
        cmd.extend(['info', '--machine-readable-json', '--installed',
 | 
					        cmd.extend(['info', '--machine-readable-json', '--installed',
 | 
				
			||||||
            '--available', '--upgrades', '--extras'])
 | 
					            '--available', '--upgrades', '--extras'])
 | 
				
			||||||
            output = subprocess.check_output(cmd)
 | 
					        proc = await asyncio.create_subprocess_exec(
 | 
				
			||||||
 | 
					            *cmd,
 | 
				
			||||||
 | 
					            stdout=asyncio.subprocess.PIPE,
 | 
				
			||||||
 | 
					            stderr=asyncio.subprocess.PIPE)
 | 
				
			||||||
 | 
					        output, stderr = await proc.communicate()
 | 
				
			||||||
 | 
					        output = output.decode('ASCII')
 | 
				
			||||||
 | 
					        if proc.returncode != 0:
 | 
				
			||||||
 | 
					            stderr = stderr.decode('ASCII')
 | 
				
			||||||
 | 
					            return False, stderr
 | 
				
			||||||
        # Default type is dict as we're going to replace the lists with
 | 
					        # Default type is dict as we're going to replace the lists with
 | 
				
			||||||
        # dicts shortly after
 | 
					        # dicts shortly after
 | 
				
			||||||
        tpls = collections.defaultdict(dict, json.loads(output))
 | 
					        tpls = collections.defaultdict(dict, json.loads(output))
 | 
				
			||||||
@ -288,45 +264,37 @@ class QubesTemplateApp(Gtk.Window):
 | 
				
			|||||||
            k: v for k, v in tpls['available'].items()
 | 
					            k: v for k, v in tpls['available'].items()
 | 
				
			||||||
                if k not in tpls['downgradable']}
 | 
					                if k not in tpls['downgradable']}
 | 
				
			||||||
        # Convert back to list
 | 
					        # Convert back to list
 | 
				
			||||||
            for key in tpls:
 | 
					        tpls = {k.title(): list(v.values()) for k, v in tpls.items()}
 | 
				
			||||||
                tpls[key] = list(tpls[key].values())
 | 
					        self.set_templates(tpls)
 | 
				
			||||||
            for status, seq in tpls.items():
 | 
					        return True, None
 | 
				
			||||||
                status_str = status.title()
 | 
					 | 
				
			||||||
                for entry in seq:
 | 
					 | 
				
			||||||
                    self.store.append(Template.build(
 | 
					 | 
				
			||||||
                        status_str, entry, self.action_models[status_str]))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        def finish_cb(future):
 | 
					class TemplateInstallConfirmDialog(
 | 
				
			||||||
            def callback():
 | 
					        ui_templateinstallconfirmdlg.Ui_TemplateInstallConfirmDlg,
 | 
				
			||||||
                if future.exception() is not None:
 | 
					        PyQt5.QtWidgets.QDialog):
 | 
				
			||||||
                    buf = self.infobox.get_buffer()
 | 
					    def __init__(self, actions):
 | 
				
			||||||
                    buf.set_text('Error:\n' + str(future.exception()))
 | 
					        super().__init__()
 | 
				
			||||||
                self.spinner.set_visible(False)
 | 
					        self.setupUi(self)
 | 
				
			||||||
                self.spinner.stop()
 | 
					 | 
				
			||||||
                self.scrollable_listing.set_visible(True)
 | 
					 | 
				
			||||||
            GLib.idle_add(callback)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        future = self.executor.submit(worker)
 | 
					        model = PyQt5.QtGui.QStandardItemModel()
 | 
				
			||||||
        future.add_done_callback(finish_cb)
 | 
					        model.setHorizontalHeaderLabels(Action.COL_NAMES)
 | 
				
			||||||
 | 
					        self.treeView.setModel(model)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def show_confirm(self, button=None):
 | 
					        for act in actions:
 | 
				
			||||||
        _ = button # unused
 | 
					            model.appendRow([PyQt5.QtGui.QStandardItem(x) for x in act])
 | 
				
			||||||
        actions = []
 | 
					 | 
				
			||||||
        for row in self.store:
 | 
					 | 
				
			||||||
            tpl = Template(*row)
 | 
					 | 
				
			||||||
            if tpl.status != tpl.default_status:
 | 
					 | 
				
			||||||
                actions.append(Action(tpl.status, tpl.name, tpl.evr))
 | 
					 | 
				
			||||||
        dialog = ConfirmDialog(self, actions)
 | 
					 | 
				
			||||||
        resp = dialog.run()
 | 
					 | 
				
			||||||
        dialog.destroy()
 | 
					 | 
				
			||||||
        if resp == Gtk.ResponseType.OK:
 | 
					 | 
				
			||||||
            self.do_install(actions)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def do_install(self, actions):
 | 
					class TemplateInstallProgressDialog(
 | 
				
			||||||
        dialog = ProgressDialog(self)
 | 
					        ui_templateinstallprogressdlg.Ui_TemplateInstallProgressDlg,
 | 
				
			||||||
        def worker():
 | 
					        PyQt5.QtWidgets.QDialog):
 | 
				
			||||||
            actions.sort()
 | 
					    def __init__(self, actions):
 | 
				
			||||||
            for oper, grp in itertools.groupby(actions, lambda x: x[0]):
 | 
					        super().__init__()
 | 
				
			||||||
 | 
					        self.setupUi(self)
 | 
				
			||||||
 | 
					        self.actions = actions
 | 
				
			||||||
 | 
					        self.buttonBox.hide()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def install(self):
 | 
				
			||||||
 | 
					        async def coro():
 | 
				
			||||||
 | 
					            self.actions.sort()
 | 
				
			||||||
 | 
					            for oper, grp in itertools.groupby(self.actions, lambda x: x[0]):
 | 
				
			||||||
                oper = oper.lower()
 | 
					                oper = oper.lower()
 | 
				
			||||||
                # No need to specify versions for local operations
 | 
					                # No need to specify versions for local operations
 | 
				
			||||||
                if oper in ('remove', 'purge'):
 | 
					                if oper in ('remove', 'purge'):
 | 
				
			||||||
@ -339,67 +307,108 @@ class QubesTemplateApp(Gtk.Window):
 | 
				
			|||||||
                #        the messages can be displayed in time.
 | 
					                #        the messages can be displayed in time.
 | 
				
			||||||
                envs = os.environ.copy()
 | 
					                envs = os.environ.copy()
 | 
				
			||||||
                envs['PYTHONUNBUFFERED'] = '1'
 | 
					                envs['PYTHONUNBUFFERED'] = '1'
 | 
				
			||||||
                proc = subprocess.Popen(
 | 
					                proc = await asyncio.create_subprocess_exec(
 | 
				
			||||||
                    BASE_CMD + [oper, '--'] + specs,
 | 
					                    *(BASE_CMD + [oper, '--'] + specs),
 | 
				
			||||||
                    stdout=subprocess.PIPE,
 | 
					                    stdout=asyncio.subprocess.PIPE,
 | 
				
			||||||
                    stderr=subprocess.STDOUT,
 | 
					                    stderr=asyncio.subprocess.STDOUT,
 | 
				
			||||||
                    text=True,
 | 
					 | 
				
			||||||
                    bufsize=1,
 | 
					 | 
				
			||||||
                    env=envs)
 | 
					                    env=envs)
 | 
				
			||||||
                #pylint: disable=cell-var-from-loop
 | 
					                #pylint: disable=cell-var-from-loop
 | 
				
			||||||
                for line in iter(proc.stdout.readline, ''):
 | 
					                while True:
 | 
				
			||||||
                    # Need to modify the buffers in the main thread
 | 
					                    line = await proc.stdout.readline()
 | 
				
			||||||
                    def callback():
 | 
					                    if line == b'':
 | 
				
			||||||
                        buf = dialog.infobox.get_buffer()
 | 
					                        break
 | 
				
			||||||
                        end_iter = buf.get_end_iter()
 | 
					                    line = line.decode('ASCII')
 | 
				
			||||||
                        buf.insert(end_iter, line)
 | 
					                    self.textEdit.append(line.rstrip())
 | 
				
			||||||
                    GLib.idle_add(callback)
 | 
					                if await proc.wait() != 0:
 | 
				
			||||||
                if proc.wait() != 0:
 | 
					                    self.buttonBox.show()
 | 
				
			||||||
 | 
					                    self.progressBar.setMaximum(100)
 | 
				
			||||||
 | 
					                    self.progressBar.setValue(0)
 | 
				
			||||||
                    return False
 | 
					                    return False
 | 
				
			||||||
 | 
					            self.progressBar.setMaximum(100)
 | 
				
			||||||
 | 
					            self.progressBar.setValue(100)
 | 
				
			||||||
 | 
					            self.buttonBox.show()
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
 | 
					        asyncio.create_task(coro())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        def finish_cb(future):
 | 
					class QvmTemplateWindow(
 | 
				
			||||||
            def callback():
 | 
					        ui_qvmtemplate.Ui_QubesTemplateManager,
 | 
				
			||||||
                dialog.finish(future.result())
 | 
					        PyQt5.QtWidgets.QMainWindow):
 | 
				
			||||||
                dialog.destroy()
 | 
					    def __init__(self, qt_app, qubes_app, dispatcher, parent=None):
 | 
				
			||||||
                self.refresh()
 | 
					        _ = parent # unused
 | 
				
			||||||
            GLib.idle_add(callback)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        future = self.executor.submit(worker)
 | 
					        super().__init__()
 | 
				
			||||||
        future.add_done_callback(finish_cb)
 | 
					        self.setupUi(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update_info(self, sel):
 | 
					        self.qubes_app = qubes_app
 | 
				
			||||||
        model, treeiters = sel.get_selected_rows()
 | 
					        self.qt_app = qt_app
 | 
				
			||||||
        if not treeiters:
 | 
					        self.dispatcher = dispatcher
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.listing_model = TemplateModel()
 | 
				
			||||||
 | 
					        self.listing_delegate = TemplateStatusDelegate(self.listing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.listing.setModel(self.listing_model)
 | 
				
			||||||
 | 
					        self.listing.setItemDelegateForColumn(0, self.listing_delegate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.refresh(False)
 | 
				
			||||||
 | 
					        self.listing.setItemDelegateForColumn(0, self.listing_delegate)
 | 
				
			||||||
 | 
					        self.listing.selectionModel() \
 | 
				
			||||||
 | 
					            .selectionChanged.connect(self.update_info)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.actionRefresh.triggered.connect(lambda: self.refresh(True))
 | 
				
			||||||
 | 
					        self.actionInstall.triggered.connect(self.do_install)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def update_info(self, selected):
 | 
				
			||||||
 | 
					        _ = selected # unused
 | 
				
			||||||
 | 
					        indices = [
 | 
				
			||||||
 | 
					            x
 | 
				
			||||||
 | 
					            for x in self.listing.selectionModel().selectedIndexes()
 | 
				
			||||||
 | 
					            if x.column() == 0]
 | 
				
			||||||
 | 
					        if len(indices) == 0:
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        buf = self.infobox.get_buffer()
 | 
					        self.infobox.clear()
 | 
				
			||||||
        if len(treeiters) > 1:
 | 
					        cursor = PyQt5.QtGui.QTextCursor(self.infobox.document())
 | 
				
			||||||
            def row_to_spec(row):
 | 
					        bold_fmt = PyQt5.QtGui.QTextCharFormat()
 | 
				
			||||||
                tpl = Template(*row)
 | 
					        bold_fmt.setFontWeight(PyQt5.QtGui.QFont.Bold)
 | 
				
			||||||
                return tpl.name + '-' + tpl.evr
 | 
					        norm_fmt = PyQt5.QtGui.QTextCharFormat()
 | 
				
			||||||
            text = '\n'.join(row_to_spec(model[it]) for it in treeiters)
 | 
					        if len(indices) > 1:
 | 
				
			||||||
            buf.set_text('Selected templates:\n' + text)
 | 
					            cursor.insertText('Selected templates:\n', bold_fmt)
 | 
				
			||||||
 | 
					            for idx in indices:
 | 
				
			||||||
 | 
					                tpl = self.listing_model.children[idx.row()]
 | 
				
			||||||
 | 
					                cursor.insertText(tpl.name + '-' + tpl.evr + '\n', norm_fmt)
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            itr = treeiters[0]
 | 
					            idx = indices[0]
 | 
				
			||||||
            tpl = Template(*model[itr])
 | 
					            tpl = self.listing_model.children[idx.row()]
 | 
				
			||||||
            text = 'Name: %s\n\nDescription:\n%s' % (tpl.name, tpl.description)
 | 
					            cursor.insertText('Name: ', bold_fmt)
 | 
				
			||||||
            buf.set_text(text)
 | 
					            cursor.insertText(tpl.name + '\n', norm_fmt)
 | 
				
			||||||
 | 
					            cursor.insertText('Description:\n', bold_fmt)
 | 
				
			||||||
 | 
					            cursor.insertText(tpl.description + '\n', norm_fmt)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def entry_edit(self, widget, path, text):
 | 
					    def refresh(self, refresh=True):
 | 
				
			||||||
        _ = widget # unused
 | 
					        self.progressBar.show()
 | 
				
			||||||
        #pylint: disable=unsubscriptable-object
 | 
					        async def coro():
 | 
				
			||||||
        tpl = Template(*self.store[path])
 | 
					            ok, stderr = await self.listing_model.refresh(refresh)
 | 
				
			||||||
        tpl = tpl._replace(status=text)
 | 
					            self.infobox.clear()
 | 
				
			||||||
        if text == tpl.default_status:
 | 
					            if not ok:
 | 
				
			||||||
            tpl = tpl._replace(weight=Pango.Weight.BOOK)
 | 
					                cursor = PyQt5.QtGui.QTextCursor(self.infobox.document())
 | 
				
			||||||
        else:
 | 
					                fmt = PyQt5.QtGui.QTextCharFormat()
 | 
				
			||||||
            tpl = tpl._replace(weight=Pango.Weight.BOLD)
 | 
					                fmt.setFontWeight(PyQt5.QtGui.QFont.Bold)
 | 
				
			||||||
        #pylint: disable=unsupported-assignment-operation
 | 
					                cursor.insertText('Failed to fetch template list:\n', fmt)
 | 
				
			||||||
        self.store[path] = tpl
 | 
					                fmt.setFontWeight(PyQt5.QtGui.QFont.Normal)
 | 
				
			||||||
 | 
					                cursor.insertText(stderr, fmt)
 | 
				
			||||||
 | 
					            self.progressBar.hide()
 | 
				
			||||||
 | 
					        asyncio.create_task(coro())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def do_install(self):
 | 
				
			||||||
 | 
					        actions = self.listing_model.get_actions()
 | 
				
			||||||
 | 
					        confirm = TemplateInstallConfirmDialog(actions)
 | 
				
			||||||
 | 
					        if confirm.exec_():
 | 
				
			||||||
 | 
					            progress = TemplateInstallProgressDialog(actions)
 | 
				
			||||||
 | 
					            progress.install()
 | 
				
			||||||
 | 
					            progress.exec_()
 | 
				
			||||||
 | 
					        self.refresh()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main():
 | 
				
			||||||
 | 
					    utils.run_asynchronous(QvmTemplateWindow)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == '__main__':
 | 
					if __name__ == '__main__':
 | 
				
			||||||
    main = QubesTemplateApp()
 | 
					    main()
 | 
				
			||||||
    main.connect('destroy', Gtk.main_quit)
 | 
					 | 
				
			||||||
    main.show_all()
 | 
					 | 
				
			||||||
    main.refresh()
 | 
					 | 
				
			||||||
    Gtk.main()
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										104
									
								
								ui/qvmtemplate.ui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								ui/qvmtemplate.ui
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,104 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<ui version="4.0">
 | 
				
			||||||
 | 
					 <class>QubesTemplateManager</class>
 | 
				
			||||||
 | 
					 <widget class="QMainWindow" name="QubesTemplateManager">
 | 
				
			||||||
 | 
					  <property name="geometry">
 | 
				
			||||||
 | 
					   <rect>
 | 
				
			||||||
 | 
					    <x>0</x>
 | 
				
			||||||
 | 
					    <y>0</y>
 | 
				
			||||||
 | 
					    <width>465</width>
 | 
				
			||||||
 | 
					    <height>478</height>
 | 
				
			||||||
 | 
					   </rect>
 | 
				
			||||||
 | 
					  </property>
 | 
				
			||||||
 | 
					  <property name="windowTitle">
 | 
				
			||||||
 | 
					   <string>Qubes Template Manager</string>
 | 
				
			||||||
 | 
					  </property>
 | 
				
			||||||
 | 
					  <widget class="QWidget" name="centralwidget">
 | 
				
			||||||
 | 
					   <layout class="QVBoxLayout" name="verticalLayout">
 | 
				
			||||||
 | 
					    <item>
 | 
				
			||||||
 | 
					     <widget class="QProgressBar" name="progressBar">
 | 
				
			||||||
 | 
					      <property name="maximum">
 | 
				
			||||||
 | 
					       <number>0</number>
 | 
				
			||||||
 | 
					      </property>
 | 
				
			||||||
 | 
					      <property name="value">
 | 
				
			||||||
 | 
					       <number>-1</number>
 | 
				
			||||||
 | 
					      </property>
 | 
				
			||||||
 | 
					     </widget>
 | 
				
			||||||
 | 
					    </item>
 | 
				
			||||||
 | 
					    <item>
 | 
				
			||||||
 | 
					     <widget class="QTreeView" name="listing">
 | 
				
			||||||
 | 
					      <property name="editTriggers">
 | 
				
			||||||
 | 
					       <set>QAbstractItemView::AllEditTriggers</set>
 | 
				
			||||||
 | 
					      </property>
 | 
				
			||||||
 | 
					      <property name="selectionMode">
 | 
				
			||||||
 | 
					       <enum>QAbstractItemView::MultiSelection</enum>
 | 
				
			||||||
 | 
					      </property>
 | 
				
			||||||
 | 
					      <property name="itemsExpandable">
 | 
				
			||||||
 | 
					       <bool>false</bool>
 | 
				
			||||||
 | 
					      </property>
 | 
				
			||||||
 | 
					      <property name="sortingEnabled">
 | 
				
			||||||
 | 
					       <bool>true</bool>
 | 
				
			||||||
 | 
					      </property>
 | 
				
			||||||
 | 
					      <property name="expandsOnDoubleClick">
 | 
				
			||||||
 | 
					       <bool>false</bool>
 | 
				
			||||||
 | 
					      </property>
 | 
				
			||||||
 | 
					     </widget>
 | 
				
			||||||
 | 
					    </item>
 | 
				
			||||||
 | 
					    <item>
 | 
				
			||||||
 | 
					     <widget class="QTextEdit" name="infobox">
 | 
				
			||||||
 | 
					      <property name="readOnly">
 | 
				
			||||||
 | 
					       <bool>true</bool>
 | 
				
			||||||
 | 
					      </property>
 | 
				
			||||||
 | 
					      <property name="placeholderText">
 | 
				
			||||||
 | 
					       <string>Click the 'Status' column to change the status of templates.</string>
 | 
				
			||||||
 | 
					      </property>
 | 
				
			||||||
 | 
					     </widget>
 | 
				
			||||||
 | 
					    </item>
 | 
				
			||||||
 | 
					   </layout>
 | 
				
			||||||
 | 
					  </widget>
 | 
				
			||||||
 | 
					  <widget class="QMenuBar" name="menubar">
 | 
				
			||||||
 | 
					   <property name="geometry">
 | 
				
			||||||
 | 
					    <rect>
 | 
				
			||||||
 | 
					     <x>0</x>
 | 
				
			||||||
 | 
					     <y>0</y>
 | 
				
			||||||
 | 
					     <width>465</width>
 | 
				
			||||||
 | 
					     <height>24</height>
 | 
				
			||||||
 | 
					    </rect>
 | 
				
			||||||
 | 
					   </property>
 | 
				
			||||||
 | 
					  </widget>
 | 
				
			||||||
 | 
					  <widget class="QStatusBar" name="statusbar"/>
 | 
				
			||||||
 | 
					  <widget class="QToolBar" name="toolBar">
 | 
				
			||||||
 | 
					   <property name="windowTitle">
 | 
				
			||||||
 | 
					    <string>toolBar</string>
 | 
				
			||||||
 | 
					   </property>
 | 
				
			||||||
 | 
					   <attribute name="toolBarArea">
 | 
				
			||||||
 | 
					    <enum>TopToolBarArea</enum>
 | 
				
			||||||
 | 
					   </attribute>
 | 
				
			||||||
 | 
					   <attribute name="toolBarBreak">
 | 
				
			||||||
 | 
					    <bool>false</bool>
 | 
				
			||||||
 | 
					   </attribute>
 | 
				
			||||||
 | 
					   <addaction name="actionRefresh"/>
 | 
				
			||||||
 | 
					   <addaction name="actionInstall"/>
 | 
				
			||||||
 | 
					  </widget>
 | 
				
			||||||
 | 
					  <action name="actionRefresh">
 | 
				
			||||||
 | 
					   <property name="icon">
 | 
				
			||||||
 | 
					    <iconset theme="view-refresh">
 | 
				
			||||||
 | 
					     <normaloff>.</normaloff>.</iconset>
 | 
				
			||||||
 | 
					   </property>
 | 
				
			||||||
 | 
					   <property name="text">
 | 
				
			||||||
 | 
					    <string>Refresh</string>
 | 
				
			||||||
 | 
					   </property>
 | 
				
			||||||
 | 
					  </action>
 | 
				
			||||||
 | 
					  <action name="actionInstall">
 | 
				
			||||||
 | 
					   <property name="icon">
 | 
				
			||||||
 | 
					    <iconset theme="go-down">
 | 
				
			||||||
 | 
					     <normaloff>.</normaloff>.</iconset>
 | 
				
			||||||
 | 
					   </property>
 | 
				
			||||||
 | 
					   <property name="text">
 | 
				
			||||||
 | 
					    <string>Install</string>
 | 
				
			||||||
 | 
					   </property>
 | 
				
			||||||
 | 
					  </action>
 | 
				
			||||||
 | 
					 </widget>
 | 
				
			||||||
 | 
					 <resources/>
 | 
				
			||||||
 | 
					 <connections/>
 | 
				
			||||||
 | 
					</ui>
 | 
				
			||||||
							
								
								
									
										84
									
								
								ui/templateinstallconfirmdlg.ui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								ui/templateinstallconfirmdlg.ui
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<ui version="4.0">
 | 
				
			||||||
 | 
					 <class>TemplateInstallConfirmDlg</class>
 | 
				
			||||||
 | 
					 <widget class="QDialog" name="TemplateInstallConfirmDlg">
 | 
				
			||||||
 | 
					  <property name="geometry">
 | 
				
			||||||
 | 
					   <rect>
 | 
				
			||||||
 | 
					    <x>0</x>
 | 
				
			||||||
 | 
					    <y>0</y>
 | 
				
			||||||
 | 
					    <width>400</width>
 | 
				
			||||||
 | 
					    <height>290</height>
 | 
				
			||||||
 | 
					   </rect>
 | 
				
			||||||
 | 
					  </property>
 | 
				
			||||||
 | 
					  <property name="windowTitle">
 | 
				
			||||||
 | 
					   <string>Template Install Confirmation</string>
 | 
				
			||||||
 | 
					  </property>
 | 
				
			||||||
 | 
					  <layout class="QVBoxLayout" name="verticalLayout">
 | 
				
			||||||
 | 
					   <item>
 | 
				
			||||||
 | 
					    <widget class="QLabel" name="label">
 | 
				
			||||||
 | 
					     <property name="text">
 | 
				
			||||||
 | 
					      <string><html><head/><body><p><span style=" font-weight:600;">WARNING: Local changes made to the following templates will be overwritten! Continue?</span></p></body></html></string>
 | 
				
			||||||
 | 
					     </property>
 | 
				
			||||||
 | 
					     <property name="wordWrap">
 | 
				
			||||||
 | 
					      <bool>true</bool>
 | 
				
			||||||
 | 
					     </property>
 | 
				
			||||||
 | 
					    </widget>
 | 
				
			||||||
 | 
					   </item>
 | 
				
			||||||
 | 
					   <item>
 | 
				
			||||||
 | 
					    <widget class="QTreeView" name="treeView">
 | 
				
			||||||
 | 
					     <property name="sortingEnabled">
 | 
				
			||||||
 | 
					      <bool>true</bool>
 | 
				
			||||||
 | 
					     </property>
 | 
				
			||||||
 | 
					     <property name="expandsOnDoubleClick">
 | 
				
			||||||
 | 
					      <bool>false</bool>
 | 
				
			||||||
 | 
					     </property>
 | 
				
			||||||
 | 
					    </widget>
 | 
				
			||||||
 | 
					   </item>
 | 
				
			||||||
 | 
					   <item>
 | 
				
			||||||
 | 
					    <widget class="QDialogButtonBox" name="buttonBox">
 | 
				
			||||||
 | 
					     <property name="orientation">
 | 
				
			||||||
 | 
					      <enum>Qt::Horizontal</enum>
 | 
				
			||||||
 | 
					     </property>
 | 
				
			||||||
 | 
					     <property name="standardButtons">
 | 
				
			||||||
 | 
					      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
 | 
				
			||||||
 | 
					     </property>
 | 
				
			||||||
 | 
					    </widget>
 | 
				
			||||||
 | 
					   </item>
 | 
				
			||||||
 | 
					  </layout>
 | 
				
			||||||
 | 
					 </widget>
 | 
				
			||||||
 | 
					 <resources/>
 | 
				
			||||||
 | 
					 <connections>
 | 
				
			||||||
 | 
					  <connection>
 | 
				
			||||||
 | 
					   <sender>buttonBox</sender>
 | 
				
			||||||
 | 
					   <signal>accepted()</signal>
 | 
				
			||||||
 | 
					   <receiver>TemplateInstallConfirmDlg</receiver>
 | 
				
			||||||
 | 
					   <slot>accept()</slot>
 | 
				
			||||||
 | 
					   <hints>
 | 
				
			||||||
 | 
					    <hint type="sourcelabel">
 | 
				
			||||||
 | 
					     <x>248</x>
 | 
				
			||||||
 | 
					     <y>254</y>
 | 
				
			||||||
 | 
					    </hint>
 | 
				
			||||||
 | 
					    <hint type="destinationlabel">
 | 
				
			||||||
 | 
					     <x>157</x>
 | 
				
			||||||
 | 
					     <y>274</y>
 | 
				
			||||||
 | 
					    </hint>
 | 
				
			||||||
 | 
					   </hints>
 | 
				
			||||||
 | 
					  </connection>
 | 
				
			||||||
 | 
					  <connection>
 | 
				
			||||||
 | 
					   <sender>buttonBox</sender>
 | 
				
			||||||
 | 
					   <signal>rejected()</signal>
 | 
				
			||||||
 | 
					   <receiver>TemplateInstallConfirmDlg</receiver>
 | 
				
			||||||
 | 
					   <slot>reject()</slot>
 | 
				
			||||||
 | 
					   <hints>
 | 
				
			||||||
 | 
					    <hint type="sourcelabel">
 | 
				
			||||||
 | 
					     <x>316</x>
 | 
				
			||||||
 | 
					     <y>260</y>
 | 
				
			||||||
 | 
					    </hint>
 | 
				
			||||||
 | 
					    <hint type="destinationlabel">
 | 
				
			||||||
 | 
					     <x>286</x>
 | 
				
			||||||
 | 
					     <y>274</y>
 | 
				
			||||||
 | 
					    </hint>
 | 
				
			||||||
 | 
					   </hints>
 | 
				
			||||||
 | 
					  </connection>
 | 
				
			||||||
 | 
					 </connections>
 | 
				
			||||||
 | 
					</ui>
 | 
				
			||||||
							
								
								
									
										81
									
								
								ui/templateinstallprogressdlg.ui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								ui/templateinstallprogressdlg.ui
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<ui version="4.0">
 | 
				
			||||||
 | 
					 <class>TemplateInstallProgressDlg</class>
 | 
				
			||||||
 | 
					 <widget class="QDialog" name="TemplateInstallProgressDlg">
 | 
				
			||||||
 | 
					  <property name="geometry">
 | 
				
			||||||
 | 
					   <rect>
 | 
				
			||||||
 | 
					    <x>0</x>
 | 
				
			||||||
 | 
					    <y>0</y>
 | 
				
			||||||
 | 
					    <width>400</width>
 | 
				
			||||||
 | 
					    <height>300</height>
 | 
				
			||||||
 | 
					   </rect>
 | 
				
			||||||
 | 
					  </property>
 | 
				
			||||||
 | 
					  <property name="windowTitle">
 | 
				
			||||||
 | 
					   <string>Installing Templates...</string>
 | 
				
			||||||
 | 
					  </property>
 | 
				
			||||||
 | 
					  <layout class="QVBoxLayout" name="verticalLayout">
 | 
				
			||||||
 | 
					   <item>
 | 
				
			||||||
 | 
					    <widget class="QProgressBar" name="progressBar">
 | 
				
			||||||
 | 
					     <property name="maximum">
 | 
				
			||||||
 | 
					      <number>0</number>
 | 
				
			||||||
 | 
					     </property>
 | 
				
			||||||
 | 
					     <property name="value">
 | 
				
			||||||
 | 
					      <number>-1</number>
 | 
				
			||||||
 | 
					     </property>
 | 
				
			||||||
 | 
					    </widget>
 | 
				
			||||||
 | 
					   </item>
 | 
				
			||||||
 | 
					   <item>
 | 
				
			||||||
 | 
					    <widget class="QTextEdit" name="textEdit">
 | 
				
			||||||
 | 
					     <property name="readOnly">
 | 
				
			||||||
 | 
					      <bool>true</bool>
 | 
				
			||||||
 | 
					     </property>
 | 
				
			||||||
 | 
					    </widget>
 | 
				
			||||||
 | 
					   </item>
 | 
				
			||||||
 | 
					   <item>
 | 
				
			||||||
 | 
					    <widget class="QDialogButtonBox" name="buttonBox">
 | 
				
			||||||
 | 
					     <property name="orientation">
 | 
				
			||||||
 | 
					      <enum>Qt::Horizontal</enum>
 | 
				
			||||||
 | 
					     </property>
 | 
				
			||||||
 | 
					     <property name="standardButtons">
 | 
				
			||||||
 | 
					      <set>QDialogButtonBox::Ok</set>
 | 
				
			||||||
 | 
					     </property>
 | 
				
			||||||
 | 
					    </widget>
 | 
				
			||||||
 | 
					   </item>
 | 
				
			||||||
 | 
					  </layout>
 | 
				
			||||||
 | 
					 </widget>
 | 
				
			||||||
 | 
					 <resources/>
 | 
				
			||||||
 | 
					 <connections>
 | 
				
			||||||
 | 
					  <connection>
 | 
				
			||||||
 | 
					   <sender>buttonBox</sender>
 | 
				
			||||||
 | 
					   <signal>accepted()</signal>
 | 
				
			||||||
 | 
					   <receiver>TemplateInstallProgressDlg</receiver>
 | 
				
			||||||
 | 
					   <slot>accept()</slot>
 | 
				
			||||||
 | 
					   <hints>
 | 
				
			||||||
 | 
					    <hint type="sourcelabel">
 | 
				
			||||||
 | 
					     <x>248</x>
 | 
				
			||||||
 | 
					     <y>254</y>
 | 
				
			||||||
 | 
					    </hint>
 | 
				
			||||||
 | 
					    <hint type="destinationlabel">
 | 
				
			||||||
 | 
					     <x>157</x>
 | 
				
			||||||
 | 
					     <y>274</y>
 | 
				
			||||||
 | 
					    </hint>
 | 
				
			||||||
 | 
					   </hints>
 | 
				
			||||||
 | 
					  </connection>
 | 
				
			||||||
 | 
					  <connection>
 | 
				
			||||||
 | 
					   <sender>buttonBox</sender>
 | 
				
			||||||
 | 
					   <signal>rejected()</signal>
 | 
				
			||||||
 | 
					   <receiver>TemplateInstallProgressDlg</receiver>
 | 
				
			||||||
 | 
					   <slot>reject()</slot>
 | 
				
			||||||
 | 
					   <hints>
 | 
				
			||||||
 | 
					    <hint type="sourcelabel">
 | 
				
			||||||
 | 
					     <x>316</x>
 | 
				
			||||||
 | 
					     <y>260</y>
 | 
				
			||||||
 | 
					    </hint>
 | 
				
			||||||
 | 
					    <hint type="destinationlabel">
 | 
				
			||||||
 | 
					     <x>286</x>
 | 
				
			||||||
 | 
					     <y>274</y>
 | 
				
			||||||
 | 
					    </hint>
 | 
				
			||||||
 | 
					   </hints>
 | 
				
			||||||
 | 
					  </connection>
 | 
				
			||||||
 | 
					 </connections>
 | 
				
			||||||
 | 
					</ui>
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user