qvm-template: Port initial code to PyQt.

This commit is contained in:
WillyPillow 2020-08-25 00:17:24 +08:00
parent 79b6d8f72c
commit 7e8ee7e8cc
No known key found for this signature in database
GPG Key ID: 3839E194B1415A9C
4 changed files with 574 additions and 296 deletions

View File

@ -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,253 +71,230 @@ 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):
OPS = [
['Installed', 'Reinstall', 'Remove'],
['Extra', 'Remove'],
['Upgradable', 'Upgrade', 'Remove'],
['Downgradable', 'Downgrade', 'Remove'],
['Available', 'Install']
]
class ConfirmDialog(Gtk.Dialog): def createEditor(self, parent, option, index):
def __init__(self, parent, actions): _ = option # unused
super(ConfirmDialog, self).__init__( editor = PyQt5.QtWidgets.QComboBox(parent)
title='Confirmation', transient_for=parent, modal=True) # Otherwise the internalPointer can be overwritten with a QComboBox
self.add_buttons( index = index.model().index(index.row(), index.column())
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, kind = index.internalPointer().default_status
Gtk.STOCK_OK, Gtk.ResponseType.OK) for op_list in TemplateStatusDelegate.OPS:
if op_list[0] == kind:
for op in op_list:
editor.addItem(op)
editor.currentIndexChanged.connect(self.currentIndexChanged)
editor.showPopup()
return editor
return None
box = self.get_content_area() def setEditorData(self, editor, index):
self.msg = Gtk.Label() #pylint: disable=no-self-use
self.msg.set_markup(( cur = index.data()
'<b>WARNING: Local changes made to the following' idx = editor.findText(cur)
' templates will be overwritten! Continue?</b>')) if idx >= 0:
box.add(self.msg) editor.setCurrentIndex(idx)
self.store = Gtk.ListStore(*Action.TYPES) def setModelData(self, editor, model, index):
self.listing = Gtk.TreeView(model=self.store) #pylint: disable=no-self-use
for idx, colname in enumerate(Action.COL_NAMES): model.setData(index, editor.currentText())
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: def updateEditorGeometry(self, editor, option, index):
self.store.append(row) #pylint: disable=no-self-use
_ = index # unused
editor.setGeometry(option.rect)
self.scrollable_listing = Gtk.ScrolledWindow() @PyQt5.QtCore.pyqtSlot()
self.scrollable_listing.add(self.listing) def currentIndexChanged(self):
box.pack_start(self.scrollable_listing, True, True, 16) self.commitData.emit(self.sender())
self.show_all() class TemplateModel(PyQt5.QtCore.QAbstractItemModel):
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): def __init__(self):
super(QubesTemplateApp, self).__init__(title='Qubes Template Manager') super().__init__()
self.iconsize = Gtk.IconSize.SMALL_TOOLBAR self.children = []
self.executor = concurrent.futures.ThreadPoolExecutor() def flags(self, index):
self.outerbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) if index.isValid() and index.column() == 0:
self.__build_action_models() return super().flags(index) | PyQt5.QtCore.Qt.ItemIsEditable
self.__build_toolbar() return super().flags(index)
self.__build_listing()
self.__build_infobox()
self.add(self.outerbox) def sort(self, idx, order):
rev = (order == PyQt5.QtCore.Qt.AscendingOrder)
self.children.sort(key=lambda x: x[idx], reverse=rev)
def __build_action_models(self): self.dataChanged.emit(*self.row_index(0, self.rowCount() - 1))
#pylint: disable=invalid-name
OPS = [
['Installed', 'Reinstall', 'Remove'],
['Extra', 'Remove'],
['Upgradable', 'Upgrade', 'Remove'],
['Downgradable', 'Downgrade', 'Remove'],
['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 index(self, row, column, parent=PyQt5.QtCore.QModelIndex()):
self.toolbar = Gtk.Toolbar() if not self.hasIndex(row, column, parent):
self.btn_refresh = Gtk.ToolButton( return PyQt5.QtCore.QModelIndex()
icon_widget=Gtk.Image.new_from_icon_name(
'view-refresh', self.iconsize),
label='Refresh')
self.btn_refresh.connect('clicked', self.refresh)
self.toolbar.insert(self.btn_refresh, 0)
self.btn_install = Gtk.ToolButton( return self.createIndex(row, column, self.children[row])
icon_widget=Gtk.Image.new_from_icon_name('go-down', self.iconsize),
label='Apply')
self.btn_install.connect('clicked', self.show_confirm)
self.toolbar.insert(self.btn_install, 1)
self.outerbox.pack_start(self.toolbar, False, True, 0) def parent(self, child):
#pylint: disable=no-self-use
_ = child # unused
return PyQt5.QtCore.QModelIndex()
def __build_listing(self): def rowCount(self, parent=PyQt5.QtCore.QModelIndex()):
self.store = Gtk.ListStore(*Template.TYPES) #pylint: disable=no-self-use
_ = parent # unused
return len(self.children)
self.listing = Gtk.TreeView(model=self.store) def columnCount(self, parent=PyQt5.QtCore.QModelIndex()):
self.cols = [] #pylint: disable=no-self-use
for idx, colname in enumerate(Template.COL_NAMES): _ = parent # unused
if colname == 'Status': return len(Template.COL_NAMES)
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() def hasChildren(self, index=PyQt5.QtCore.QModelIndex()):
self.scrollable_listing.add(self.listing) #pylint: disable=no-self-use
self.scrollable_listing.set_visible(False) return index == PyQt5.QtCore.QModelIndex()
self.spinner = Gtk.Spinner() 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
self.outerbox.pack_start(self.scrollable_listing, True, True, 0) def setData(self, index, value, role=PyQt5.QtCore.Qt.EditRole):
self.outerbox.pack_start(self.spinner, True, True, 0) 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 __build_infobox(self): def headerData(self, section, orientation,
self.infobox = Gtk.TextView() role=PyQt5.QtCore.Qt.DisplayRole):
self.outerbox.pack_start(self.infobox, True, True, 16) #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 refresh(self, button=None): def removeRows(self, row, count, parent=PyQt5.QtCore.QModelIndex()):
# Ignore if we're already doing a refresh _ = parent # unused
#pylint: disable=no-member self.beginRemoveRows(PyQt5.QtCore.QModelIndex(), row, row + count)
if self.spinner.props.active: del self.children[row:row+count]
return self.endRemoveRows()
self.scrollable_listing.set_visible(False) self.dataChanged.emit(*self.row_index(row, row + count))
self.spinner.start()
self.spinner.set_visible(True)
self.store.clear()
def worker():
cmd = BASE_CMD[:]
if button is not None:
# Force refresh if triggered by button press
cmd.append('--refresh')
cmd.extend(['info', '--machine-readable-json', '--installed',
'--available', '--upgrades', '--extras'])
output = subprocess.check_output(cmd)
# Default type is dict as we're going to replace the lists with
# dicts shortly after
tpls = collections.defaultdict(dict, json.loads(output))
# Remove duplicates
# Should this be done in qvm-template?
# TODO: Merge templates with same name?
# If so, we may need to have a separate UI to force versions.
local_names = set(x['name'] for x in tpls['installed'])
# Convert to dict for easier subtraction
for key in tpls:
tpls[key] = {
(x['name'], x['epoch'], x['version'], x['release']): x
for x in tpls[key]}
tpls['installed'] = {
k: v for k, v in tpls['installed'].items()
if k not in tpls['extra'] and k not in tpls['upgradable']}
tpls['available'] = {
k: v for k, v in tpls['available'].items()
if k not in tpls['installed']
and k not in tpls['upgradable']}
# If the package name is installed but the specific version is
# neither installed or an upgrade, then it must be a downgrade
tpls['downgradable'] = {
k: v for k, v in tpls['available'].items()
if k[0] in local_names}
tpls['available'] = {
k: v for k, v in tpls['available'].items()
if k not in tpls['downgradable']}
# Convert back to list
for key in tpls:
tpls[key] = list(tpls[key].values())
for status, seq in tpls.items():
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): def row_index(self, low, high):
def callback(): return self.createIndex(low, 0), \
if future.exception() is not None: self.createIndex(high, self.columnCount())
buf = self.infobox.get_buffer()
buf.set_text('Error:\n' + str(future.exception()))
self.spinner.set_visible(False)
self.spinner.stop()
self.scrollable_listing.set_visible(True)
GLib.idle_add(callback)
future = self.executor.submit(worker) def set_templates(self, templates):
future.add_done_callback(finish_cb) 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 show_confirm(self, button=None): def get_actions(self):
_ = button # unused
actions = [] actions = []
for row in self.store: for tpl in self.children:
tpl = Template(*row)
if tpl.status != tpl.default_status: if tpl.status != tpl.default_status:
actions.append(Action(tpl.status, tpl.name, tpl.evr)) actions.append(Action(tpl.status, tpl.name, tpl.evr))
dialog = ConfirmDialog(self, actions) return actions
resp = dialog.run()
dialog.destroy()
if resp == Gtk.ResponseType.OK:
self.do_install(actions)
def do_install(self, actions): async def refresh(self, refresh=True):
dialog = ProgressDialog(self) cmd = BASE_CMD[:]
def worker(): if refresh:
actions.sort() # Force refresh if triggered by button press
for oper, grp in itertools.groupby(actions, lambda x: x[0]): cmd.append('--refresh')
cmd.extend(['info', '--machine-readable-json', '--installed',
'--available', '--upgrades', '--extras'])
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
# dicts shortly after
tpls = collections.defaultdict(dict, json.loads(output))
# Remove duplicates
# Should this be done in qvm-template?
# TODO: Merge templates with same name?
# If so, we may need to have a separate UI to force versions.
local_names = set(x['name'] for x in tpls['installed'])
# Convert to dict for easier subtraction
for key in tpls:
tpls[key] = {
(x['name'], x['epoch'], x['version'], x['release']): x
for x in tpls[key]}
tpls['installed'] = {
k: v for k, v in tpls['installed'].items()
if k not in tpls['extra'] and k not in tpls['upgradable']}
tpls['available'] = {
k: v for k, v in tpls['available'].items()
if k not in tpls['installed']
and k not in tpls['upgradable']}
# If the package name is installed but the specific version is
# neither installed or an upgrade, then it must be a downgrade
tpls['downgradable'] = {
k: v for k, v in tpls['available'].items()
if k[0] in local_names}
tpls['available'] = {
k: v for k, v in tpls['available'].items()
if k not in tpls['downgradable']}
# Convert back to list
tpls = {k.title(): list(v.values()) for k, v in tpls.items()}
self.set_templates(tpls)
return True, None
class TemplateInstallConfirmDialog(
ui_templateinstallconfirmdlg.Ui_TemplateInstallConfirmDlg,
PyQt5.QtWidgets.QDialog):
def __init__(self, actions):
super().__init__()
self.setupUi(self)
model = PyQt5.QtGui.QStandardItemModel()
model.setHorizontalHeaderLabels(Action.COL_NAMES)
self.treeView.setModel(model)
for act in actions:
model.appendRow([PyQt5.QtGui.QStandardItem(x) for x in act])
class TemplateInstallProgressDialog(
ui_templateinstallprogressdlg.Ui_TemplateInstallProgressDlg,
PyQt5.QtWidgets.QDialog):
def __init__(self, actions):
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
View 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>

View 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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;WARNING: Local changes made to the following templates will be overwritten! Continue?&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>

View 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>