1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2024-10-29 21:05:57 +00:00

More cleanups

- Cleanup columns.py and tables
- Other misc cleanups
- Remove text_field.py from qtlib as it is not used
- Remove unused variables from image_viewer method
This commit is contained in:
Andrew Senetar 2021-08-25 00:46:33 -05:00
parent 2e13c4ccb5
commit f11fccc889
Signed by: arsenetar
GPG Key ID: C63300DCE48AB2F1
22 changed files with 146 additions and 163 deletions

View File

@ -264,7 +264,7 @@ class DupeGuru(Broadcaster):
return None
def _get_export_data(self):
columns = [col for col in self.result_table.columns.ordered_columns if col.visible and col.name != "marked"]
columns = [col for col in self.result_table._columns.ordered_columns if col.visible and col.name != "marked"]
colnames = [col.display for col in columns]
rows = []
for group_id, group in enumerate(self.results.groups):

View File

@ -16,7 +16,7 @@ class ExcludeListTable(GUITable, DupeGuruGUIObject):
def __init__(self, exclude_list_dialog, app):
GUITable.__init__(self)
DupeGuruGUIObject.__init__(self, app)
self.columns = Columns(self)
self._columns = Columns(self)
self.dialog = exclude_list_dialog
def rename_selected(self, newname):

View File

@ -22,7 +22,7 @@ class IgnoreListTable(GUITable):
def __init__(self, ignore_list_dialog):
GUITable.__init__(self)
self.columns = Columns(self)
self._columns = Columns(self)
self.view = None
self.dialog = ignore_list_dialog

View File

@ -21,7 +21,7 @@ class ProblemTable(GUITable):
def __init__(self, problem_dialog):
GUITable.__init__(self)
self.columns = Columns(self)
self._columns = Columns(self)
self.dialog = problem_dialog
# --- Override

View File

@ -82,7 +82,7 @@ class ResultTable(GUITable, DupeGuruGUIObject):
def __init__(self, app):
GUITable.__init__(self)
DupeGuruGUIObject.__init__(self, app)
self.columns = Columns(self, prefaccess=app, savename="ResultTable")
self._columns = Columns(self, prefaccess=app, savename="ResultTable")
self._power_marker = False
self._delta_values = False
self._sort_descriptors = ("name", True)
@ -190,4 +190,4 @@ class ResultTable(GUITable, DupeGuruGUIObject):
self.view.refresh()
def save_session(self):
self.columns.save_columns()
self._columns.save_columns()

View File

@ -17,9 +17,11 @@ class Markable:
# in self.__marked, and is not affected by __inverted. Thus, self.mark while __inverted
# is True will launch _DidUnmark.
def _did_mark(self, o):
# Implemented in child classes
pass
def _did_unmark(self, o):
# Implemented in child classes
pass
def _get_markable_count(self):

View File

@ -151,8 +151,8 @@ class TestApp(TestAppBase):
def __init__(self):
def link_gui(gui):
gui.view = self.make_logger()
if hasattr(gui, "columns"): # tables
gui.columns.view = self.make_logger()
if hasattr(gui, "_columns"): # tables
gui._columns.view = self.make_logger()
return gui
TestAppBase.__init__(self)

View File

@ -290,8 +290,10 @@ class TestCaseDeleteIfEmpty:
class TestCaseOpenIfFilename:
FILE_NAME = "test.txt"
def test_file_name(self, tmpdir):
filepath = str(tmpdir.join("test.txt"))
filepath = str(tmpdir.join(self.FILE_NAME))
open(filepath, "wb").write(b"test_data")
file, close = open_if_filename(filepath)
assert close
@ -307,7 +309,7 @@ class TestCaseOpenIfFilename:
eq_("test_data", file.read())
def test_mode_is_passed_to_open(self, tmpdir):
filepath = str(tmpdir.join("test.txt"))
filepath = str(tmpdir.join(self.FILE_NAME))
open(filepath, "w").close()
file, close = open_if_filename(filepath, "a")
eq_("a", file.mode)
@ -315,8 +317,10 @@ class TestCaseOpenIfFilename:
class TestCaseFileOrPath:
FILE_NAME = "test.txt"
def test_path(self, tmpdir):
filepath = str(tmpdir.join("test.txt"))
filepath = str(tmpdir.join(self.FILE_NAME))
open(filepath, "wb").write(b"test_data")
with FileOrPath(filepath) as fp:
eq_(b"test_data", fp.read())
@ -329,7 +333,7 @@ class TestCaseFileOrPath:
eq_("test_data", fp.read())
def test_mode_is_passed_to_open(self, tmpdir):
filepath = str(tmpdir.join("test.txt"))
filepath = str(tmpdir.join(self.FILE_NAME))
open(filepath, "w").close()
with FileOrPath(filepath, "a") as fp:
eq_("a", fp.mode)

View File

@ -4,6 +4,7 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from qt.platform import HELP_PATH
import sys
import os
import os.path as op
@ -26,6 +27,10 @@ from hscommon.build import (
copy_all,
)
ENTRY_SCRIPT = "run.py"
LOCALE_DIR = "build/locale"
HELP_DIR = "build/help"
def parse_args():
parser = ArgumentParser()
@ -33,6 +38,15 @@ def parse_args():
return parser.parse_args()
def check_loc_doc():
if not op.exists(LOCALE_DIR):
print('Locale files are missing. Have you run "build.py --loc"?')
# include help files if they are built otherwise exit as they should be included?
if not op.exists(HELP_DIR):
print('Help files are missing. Have you run "build.py --doc"?')
return op.exists(LOCALE_DIR) and op.exists(HELP_DIR)
def copy_files_to_package(destpath, packages, with_so):
# when with_so is true, we keep .so files in the package, and otherwise, we don't. We need this
# flag because when building debian src pkg, we *don't* want .so files (they're compiled later)
@ -40,17 +54,13 @@ def copy_files_to_package(destpath, packages, with_so):
if op.exists(destpath):
shutil.rmtree(destpath)
os.makedirs(destpath)
shutil.copy("run.py", op.join(destpath, "run.py"))
shutil.copy(ENTRY_SCRIPT, op.join(destpath, ENTRY_SCRIPT))
extra_ignores = ["*.so"] if not with_so else None
copy_packages(packages, destpath, extra_ignores=extra_ignores)
# include locale files if they are built otherwise exit as it will break
# the localization
if not op.exists("build/locale"):
print('Locale files are missing. Have you run "build.py --loc"? Exiting...')
return
# include help files if they are built otherwise exit as they should be included?
if not op.exists("build/help"):
print('Help files are missing. Have you run "build.py --doc"? Exiting...')
if not check_loc_doc():
print("Exiting...")
return
shutil.copytree(op.join("build", "help"), op.join(destpath, "help"))
shutil.copytree(op.join("build", "locale"), op.join(destpath, "locale"))
@ -152,12 +162,8 @@ def package_windows():
arch = "x86"
# include locale files if they are built otherwise exit as it will break
# the localization
if not op.exists("build/locale"):
print('Locale files are missing. Have you run "build.py --loc"? Exiting...')
return
# include help files if they are built otherwise exit as they should be included?
if not op.exists("build/help"):
print('Help files are missing. Have you run "build.py --doc"? Exiting...')
if not check_loc_doc():
print("Exiting...")
return
# create version information file from template
try:
@ -180,11 +186,11 @@ def package_windows():
"--windowed",
"--noconfirm",
"--icon=images/dgse_logo.ico",
"--add-data=build/locale;locale",
"--add-data=build/help;help",
"--add-data={0};locale".format(LOCALE_DIR),
"--add-data={0};help".format(HELP_DIR),
"--version-file=win_version_info.txt",
"--paths=C:\\Program Files (x86)\\Windows Kits\\10\\Redist\\ucrt\\DLLs\\{0}".format(arch),
"run.py",
ENTRY_SCRIPT,
]
)
# remove version info file
@ -200,12 +206,8 @@ def package_windows():
def package_macos():
# include locale files if they are built otherwise exit as it will break
# the localization
if not op.exists("build/locale"):
print('Locale files are missing. Have you run "build.py --loc"? Exiting...')
return
# include help files if they are built otherwise exit as they should be included?
if not op.exists("build/help"):
print('Help files are missing. Have you run "build.py --doc"? Exiting...')
if not check_loc_doc():
print("Exiting")
return
# run pyinstaller from here:
import PyInstaller.__main__
@ -217,9 +219,9 @@ def package_macos():
"--noconfirm",
"--icon=images/dupeguru.icns",
"--osx-bundle-identifier=com.hardcoded-software.dupeguru",
"--add-data=build/locale:locale",
"--add-data=build/help:help",
"run.py",
"--add-data={0}:locale".format(LOCALE_DIR),
"--add-data={0}:help".format(HELP_DIR),
ENTRY_SCRIPT,
]
)

View File

@ -64,6 +64,8 @@ class DirectoriesDelegate(QStyledItemDelegate):
class DirectoriesModel(TreeModel):
MIME_TYPE_FORMAT = "text/uri-list"
def __init__(self, model, view, **kwargs):
super().__init__(**kwargs)
self.model = model
@ -104,9 +106,9 @@ class DirectoriesModel(TreeModel):
def dropMimeData(self, mime_data, action, row, column, parent_index):
# the data in mimeData is urlencoded **in utf-8**
if not mime_data.hasFormat("text/uri-list"):
if not mime_data.hasFormat(self.MIME_TYPE_FORMAT):
return False
data = bytes(mime_data.data("text/uri-list")).decode("ascii")
data = bytes(mime_data.data(self.MIME_TYPE_FORMAT)).decode("ascii")
urls = data.split("\r\n")
paths = [QUrl(url).toLocalFile() for url in urls if url]
for path in paths:
@ -129,7 +131,7 @@ class DirectoriesModel(TreeModel):
return None
def mimeTypes(self):
return ["text/uri-list"]
return [self.MIME_TYPE_FORMAT]
def setData(self, index, value, role):
if not index.isValid() or role != Qt.EditRole or index.column() != 1:

View File

@ -15,7 +15,7 @@ tr = trget("ui")
class ExcludeListTable(Table):
"""Model for exclude list"""
COLUMNS = [Column("marked", defaultWidth=15), Column("regex", defaultWidth=230)]
COLUMNS = [Column("marked", default_width=15), Column("regex", default_width=230)]
def __init__(self, app, view, **kwargs):
model = app.model.exclude_list_dialog.exclude_list_table # pointer to GUITable

View File

@ -13,6 +13,6 @@ class IgnoreListTable(Table):
"""Ignore list model"""
COLUMNS = [
Column("path1", defaultWidth=230),
Column("path2", defaultWidth=230),
Column("path1", default_width=230),
Column("path2", default_width=230),
]

View File

@ -10,23 +10,23 @@ from ..results_model import ResultsModel as ResultsModelBase
class ResultsModel(ResultsModelBase):
COLUMNS = [
Column("marked", defaultWidth=30),
Column("name", defaultWidth=200),
Column("folder_path", defaultWidth=180),
Column("size", defaultWidth=60),
Column("duration", defaultWidth=60),
Column("bitrate", defaultWidth=50),
Column("samplerate", defaultWidth=60),
Column("extension", defaultWidth=40),
Column("mtime", defaultWidth=120),
Column("title", defaultWidth=120),
Column("artist", defaultWidth=120),
Column("album", defaultWidth=120),
Column("genre", defaultWidth=80),
Column("year", defaultWidth=40),
Column("track", defaultWidth=40),
Column("comment", defaultWidth=120),
Column("percentage", defaultWidth=60),
Column("words", defaultWidth=120),
Column("dupe_count", defaultWidth=80),
Column("marked", default_width=30),
Column("name", default_width=200),
Column("folder_path", default_width=180),
Column("size", default_width=60),
Column("duration", default_width=60),
Column("bitrate", default_width=50),
Column("samplerate", default_width=60),
Column("extension", default_width=40),
Column("mtime", default_width=120),
Column("title", default_width=120),
Column("artist", default_width=120),
Column("album", default_width=120),
Column("genre", default_width=80),
Column("year", default_width=40),
Column("track", default_width=40),
Column("comment", default_width=120),
Column("percentage", default_width=60),
Column("words", default_width=120),
Column("dupe_count", default_width=80),
]

View File

@ -201,16 +201,12 @@ class BaseController(QObject):
# the SelectedImageViewer widget sometimes ends up being bigger
# than the ReferenceImageViewer by one pixel, which distorts the
# scaled down pixmap for the reference, hence we'll reuse its size here.
selected_size = self._updateImage(
self.selectedPixmap, self.scaledSelectedPixmap, self.selectedViewer, None, same_group
)
self._updateImage(
self.referencePixmap, self.scaledReferencePixmap, self.referenceViewer, selected_size, same_group
)
self._updateImage(self.selectedPixmap, self.selectedViewer, same_group)
self._updateImage(self.referencePixmap, self.referenceViewer, same_group)
if ignore_update:
self.selectedViewer.ignore_signal = False
def _updateImage(self, pixmap, scaledpixmap, viewer, target_size=None, same_group=False):
def _updateImage(self, pixmap, viewer, same_group=False):
# WARNING this is called on every resize event, might need to split
# into a separate function depending on the implementation used
if pixmap.isNull():
@ -340,8 +336,8 @@ class BaseController(QObject):
self.selectedViewer.resetCenter()
self.referenceViewer.resetCenter()
target_size = self._updateImage(self.selectedPixmap, self.scaledSelectedPixmap, self.selectedViewer, None, True)
self._updateImage(self.referencePixmap, self.scaledReferencePixmap, self.referenceViewer, target_size, True)
self._updateImage(self.selectedPixmap, self.selectedViewer, True)
self._updateImage(self.referencePixmap, self.referenceViewer, True)
self.centerViews()
self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)

View File

@ -10,14 +10,14 @@ from ..results_model import ResultsModel as ResultsModelBase
class ResultsModel(ResultsModelBase):
COLUMNS = [
Column("marked", defaultWidth=30),
Column("name", defaultWidth=200),
Column("folder_path", defaultWidth=180),
Column("size", defaultWidth=60),
Column("extension", defaultWidth=40),
Column("dimensions", defaultWidth=100),
Column("exif_timestamp", defaultWidth=120),
Column("mtime", defaultWidth=120),
Column("percentage", defaultWidth=60),
Column("dupe_count", defaultWidth=80),
Column("marked", default_width=30),
Column("name", default_width=200),
Column("folder_path", default_width=180),
Column("size", default_width=60),
Column("extension", default_width=40),
Column("dimensions", default_width=100),
Column("exif_timestamp", default_width=120),
Column("mtime", default_width=120),
Column("percentage", default_width=60),
Column("dupe_count", default_width=80),
]

View File

@ -12,8 +12,8 @@ from qtlib.table import Table
class ProblemTable(Table):
COLUMNS = [
Column("path", defaultWidth=150),
Column("msg", defaultWidth=150),
Column("path", default_width=150),
Column("msg", default_width=150),
]
def __init__(self, model, view, **kwargs):

View File

@ -295,7 +295,7 @@ class ResultWindow(QMainWindow):
if menu.actions():
menu.clear()
self._column_actions = []
for index, (display, visible) in enumerate(self.app.model.result_table.columns.menu_items()):
for index, (display, visible) in enumerate(self.app.model.result_table._columns.menu_items()):
action = menu.addAction(display)
action.setCheckable(True)
action.setChecked(visible)
@ -389,7 +389,7 @@ class ResultWindow(QMainWindow):
# --- Private
def _update_column_actions_status(self):
# Update menu checked state
menu_items = self.app.model.result_table.columns.menu_items()
menu_items = self.app.model.result_table._columns.menu_items()
for action, (display, visible) in zip(self._column_actions, menu_items):
action.setChecked(visible)
@ -483,16 +483,16 @@ class ResultWindow(QMainWindow):
def columnToggled(self, action):
index = action.item_index
if index == -1:
self.app.model.result_table.columns.reset_to_defaults()
self.app.model.result_table._columns.reset_to_defaults()
self._update_column_actions_status()
else:
visible = self.app.model.result_table.columns.toggle_menu_item(index)
visible = self.app.model.result_table._columns.toggle_menu_item(index)
action.setChecked(visible)
def contextMenuEvent(self, event):
self.actionActions.menu().exec_(event.globalPos())
def resultsDoubleClicked(self, modelIndex):
def resultsDoubleClicked(self, model_index):
self.app.model.open_selected()
def resultsSpacePressed(self):

View File

@ -95,7 +95,7 @@ class ResultsModel(Table):
# --- Events
def appWillSavePrefs(self):
self.model.columns.save_columns()
self.model._columns.save_columns()
# --- model --> view
def invalidate_markings(self):

View File

@ -10,13 +10,13 @@ from ..results_model import ResultsModel as ResultsModelBase
class ResultsModel(ResultsModelBase):
COLUMNS = [
Column("marked", defaultWidth=30),
Column("name", defaultWidth=200),
Column("folder_path", defaultWidth=180),
Column("size", defaultWidth=60),
Column("extension", defaultWidth=40),
Column("mtime", defaultWidth=120),
Column("percentage", defaultWidth=60),
Column("words", defaultWidth=120),
Column("dupe_count", defaultWidth=80),
Column("marked", default_width=30),
Column("name", default_width=200),
Column("folder_path", default_width=180),
Column("size", default_width=60),
Column("extension", default_width=40),
Column("mtime", default_width=120),
Column("percentage", default_width=60),
Column("words", default_width=120),
Column("dupe_count", default_width=80),
]

View File

@ -14,38 +14,38 @@ class Column:
def __init__(
self,
attrname,
defaultWidth,
default_width,
editor=None,
alignment=Qt.AlignLeft,
cantTruncate=False,
cant_truncate=False,
painter=None,
resizeToFit=False,
resize_to_fit=False,
):
self.attrname = attrname
self.defaultWidth = defaultWidth
self.default_width = default_width
self.editor = editor
# See moneyguru #15. Painter attribute was added to allow custom painting of amount value and
# currency information. Can be used as a pattern for custom painting of any column.
self.painter = painter
self.alignment = alignment
# This is to indicate, during printing, that a column can't have its data truncated.
self.cantTruncate = cantTruncate
self.resizeToFit = resizeToFit
self.cant_truncate = cant_truncate
self.resize_to_fit = resize_to_fit
class Columns:
def __init__(self, model, columns, headerView):
def __init__(self, model, columns, header_view):
self.model = model
self._headerView = headerView
self._headerView.setDefaultAlignment(Qt.AlignLeft)
self._header_view = header_view
self._header_view.setDefaultAlignment(Qt.AlignLeft)
def setspecs(col, modelcol):
modelcol.default_width = col.defaultWidth
modelcol.default_width = col.default_width
modelcol.editor = col.editor
modelcol.painter = col.painter
modelcol.resizeToFit = col.resizeToFit
modelcol.resize_to_fit = col.resize_to_fit
modelcol.alignment = col.alignment
modelcol.cantTruncate = col.cantTruncate
modelcol.cant_truncate = col.cant_truncate
if columns:
for col in columns:
@ -56,16 +56,16 @@ class Columns:
for modelcol in self.model.column_list:
setspecs(col, modelcol)
self.model.view = self
self._headerView.sectionMoved.connect(self.headerSectionMoved)
self._headerView.sectionResized.connect(self.headerSectionResized)
self._header_view.sectionMoved.connect(self.header_section_moved)
self._header_view.sectionResized.connect(self.header_section_resized)
# See moneyguru #14 and #15. This was added in order to allow automatic resizing of columns.
for column in self.model.column_list:
if column.resizeToFit:
self._headerView.setSectionResizeMode(column.logical_index, QHeaderView.ResizeToContents)
if column.resize_to_fit:
self._header_view.setSectionResizeMode(column.logical_index, QHeaderView.ResizeToContents)
# --- Public
def setColumnsWidth(self, widths):
def set_columns_width(self, widths):
# `widths` can be None. If it is, then default widths are set.
columns = self.model.column_list
if not widths:
@ -73,38 +73,38 @@ class Columns:
for column, width in zip(columns, widths):
if width == 0: # column was hidden before.
width = column.default_width
self._headerView.resizeSection(column.logical_index, width)
self._header_view.resizeSection(column.logical_index, width)
def setColumnsOrder(self, columnIndexes):
if not columnIndexes:
def set_columns_order(self, column_indexes):
if not column_indexes:
return
for destIndex, columnIndex in enumerate(columnIndexes):
for dest_index, column_index in enumerate(column_indexes):
# moveSection takes 2 visual index arguments, so we have to get our visual index first
visualIndex = self._headerView.visualIndex(columnIndex)
self._headerView.moveSection(visualIndex, destIndex)
visual_index = self._header_view.visualIndex(column_index)
self._header_view.moveSection(visual_index, dest_index)
# --- Events
def headerSectionMoved(self, logicalIndex, oldVisualIndex, newVisualIndex):
attrname = self.model.column_by_index(logicalIndex).name
self.model.move_column(attrname, newVisualIndex)
def header_section_moved(self, logical_index, old_visual_index, new_visual_index):
attrname = self.model.column_by_index(logical_index).name
self.model.move_column(attrname, new_visual_index)
def headerSectionResized(self, logicalIndex, oldSize, newSize):
attrname = self.model.column_by_index(logicalIndex).name
self.model.resize_column(attrname, newSize)
def header_section_resized(self, logical_index, old_size, new_size):
attrname = self.model.column_by_index(logical_index).name
self.model.resize_column(attrname, new_size)
# --- model --> view
def restore_columns(self):
columns = self.model.ordered_columns
indexes = [col.logical_index for col in columns]
self.setColumnsOrder(indexes)
self.set_columns_order(indexes)
widths = [col.width for col in self.model.column_list]
if not any(widths):
widths = None
self.setColumnsWidth(widths)
self.set_columns_width(widths)
for column in self.model.column_list:
visible = self.model.column_is_visible(column.name)
self._headerView.setSectionHidden(column.logical_index, not visible)
self._header_view.setSectionHidden(column.logical_index, not visible)
def set_column_visible(self, colname, visible):
column = self.model.column_by_name(colname)
self._headerView.setSectionHidden(column.logical_index, not visible)
self._header_view.setSectionHidden(column.logical_index, not visible)

View File

@ -28,8 +28,8 @@ class Table(QAbstractTableModel):
self.view = view
self.view.setModel(self)
self.model.view = self
if hasattr(self.model, "columns"):
self.columns = Columns(self.model.columns, self.COLUMNS, view.horizontalHeader())
if hasattr(self.model, "_columns"):
self._columns = Columns(self.model._columns, self.COLUMNS, view.horizontalHeader())
self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged)
@ -82,28 +82,28 @@ class Table(QAbstractTableModel):
return False
def columnCount(self, index):
return self.model.columns.columns_count()
return self.model._columns.columns_count()
def data(self, index, role):
if not index.isValid():
return None
row = self.model[index.row()]
column = self.model.columns.column_by_index(index.column())
column = self.model._columns.column_by_index(index.column())
return self._getData(row, column, role)
def flags(self, index):
if not index.isValid():
return self.INVALID_INDEX_FLAGS
row = self.model[index.row()]
column = self.model.columns.column_by_index(index.column())
column = self.model._columns.column_by_index(index.column())
return self._getFlags(row, column)
def headerData(self, section, orientation, role):
if orientation != Qt.Horizontal:
return None
if section >= self.model.columns.columns_count():
if section >= self.model._columns.columns_count():
return None
column = self.model.columns.column_by_index(section)
column = self.model._columns.column_by_index(section)
if role == Qt.DisplayRole:
return column.display
elif role == Qt.TextAlignmentRole:
@ -123,11 +123,11 @@ class Table(QAbstractTableModel):
if not index.isValid():
return False
row = self.model[index.row()]
column = self.model.columns.column_by_index(index.column())
column = self.model._columns.column_by_index(index.column())
return self._setData(row, column, value, role)
def sort(self, section, order):
column = self.model.columns.column_by_index(section)
column = self.model._columns.column_by_index(section)
attrname = column.name
self.model.sort_by(attrname, desc=order == Qt.DescendingOrder)

View File

@ -1,23 +0,0 @@
# Created On: 2012/01/23
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
class TextField:
def __init__(self, model, view):
self.model = model
self.view = view
self.model.view = self
# Make TextField also work for QLabel, which doesn't allow editing
if hasattr(self.view, "editingFinished"):
self.view.editingFinished.connect(self.editingFinished)
def editingFinished(self):
self.model.text = self.view.text()
# model --> view
def refresh(self):
self.view.setText(self.model.text)