From fe06bd428d7718c0b512f183951afa98b155004f Mon Sep 17 00:00:00 2001 From: Linda Karlovska <49241681+lindakladivova@users.noreply.github.com> Date: Thu, 9 May 2024 15:47:49 +0200 Subject: [PATCH] wxGUI/history: Add time/status icons in front of browser nodes (#3679) --- gui/icons/grass/circle.png | Bin 0 -> 399 bytes gui/icons/grass/circle.svg | 43 ++++++++++++++++ gui/icons/grass/cross.png | Bin 0 -> 646 bytes gui/icons/grass/cross.svg | 52 +++++++++++++++++++ gui/icons/grass/exclamation-mark.png | Bin 0 -> 492 bytes gui/icons/grass/exclamation-mark.svg | 45 ++++++++++++++++ gui/icons/grass/question-mark.png | Bin 0 -> 497 bytes gui/icons/grass/question-mark.svg | 40 +++++++++++++++ gui/icons/grass/success.png | Bin 0 -> 595 bytes gui/icons/grass/success.svg | 41 +++++++++++++++ gui/icons/grass/time-period.png | Bin 0 -> 733 bytes gui/icons/grass/time-period.svg | 39 ++++++++++++++ gui/wxpython/core/gconsole.py | 42 +++++++++------ gui/wxpython/history/tree.py | 74 +++++++++++++++++++++------ gui/wxpython/lmgr/frame.py | 15 ++++-- gui/wxpython/main_window/frame.py | 15 ++++-- python/grass/grassdb/history.py | 13 +++++ 17 files changed, 383 insertions(+), 36 deletions(-) create mode 100644 gui/icons/grass/circle.png create mode 100644 gui/icons/grass/circle.svg create mode 100644 gui/icons/grass/cross.png create mode 100644 gui/icons/grass/cross.svg create mode 100644 gui/icons/grass/exclamation-mark.png create mode 100644 gui/icons/grass/exclamation-mark.svg create mode 100644 gui/icons/grass/question-mark.png create mode 100644 gui/icons/grass/question-mark.svg create mode 100644 gui/icons/grass/success.png create mode 100644 gui/icons/grass/success.svg create mode 100644 gui/icons/grass/time-period.png create mode 100644 gui/icons/grass/time-period.svg diff --git a/gui/icons/grass/circle.png b/gui/icons/grass/circle.png new file mode 100644 index 0000000000000000000000000000000000000000..cec9e99476841d78fe1f9736565f2133cf67b4cb GIT binary patch literal 399 zcmV;A0dW3_P)<62(#nhN@zs6JaSJ9;a-XOJ9U`&Bp5hWof$62~|a6KuIBg z8pX#{sfl6Wm{l?WpiGcj$_0zXjqSfbYXR$^XG=GI5a%CWle04HgwC6Djoa6B z7pu&`6~&nM<1<~qydQfTnU5;5%E3~Do$V$ut7O7dms-G2q<~6+Dj?-E%7k{aC=VQUnT6z7?xpYzVC!GL1oyP&be+FZBPP=^K02a=kDB(n|mU002ovPDHLkV1hppr=I`- literal 0 HcmV?d00001 diff --git a/gui/icons/grass/circle.svg b/gui/icons/grass/circle.svg new file mode 100644 index 00000000000..00559c46a7f --- /dev/null +++ b/gui/icons/grass/circle.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/gui/icons/grass/cross.png b/gui/icons/grass/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..d5190202eedb5208d7e871f79a3b4861409b706f GIT binary patch literal 646 zcmV;10(t$3P)reTL>Wk^<>NY-_>q_t+>AP@%or!}4-j`v#Hi7vrnHNr z!~tc2#Au9yv8(CkYP3zkg{jCD`RK*N(B5)u#5Z||^YJ|AdCz$ds*0*I1szY}NMIjy zv(P&Qc3?*UrbRjrK`{oUMAr{!AUWs+KAnNHT@WilWeqOgRjq`< zk426B375gEz``xn8=|TpB6*l|qtVO##yGmR7Lb$$A{j{A zO>h;wGR)srod9SMKtS@!wX7>CW1K&=8uex@7OGr^`N<}@onW72*0rp${_bu^YqeG= zEIZuXNZXuLbrYs4zWrsgGfdjA&e1@_I7w)K5TMC|t3@70IXfoLXFN60T7QVvU75D`Z z2S#Bq3B}-_$O9NU42u~Uj9V7!`|aHpkpjFNf~)-@B_*hA!1+igoKAqT0rh>VjmrC@ zQq@Vg{te#!2q{TGmklRSRdO)#0#;wcdLCx}xrrdtF!u($=dfCUYpO~sAqNu$c=q3Q gZjj96VLb5d0esCPMY&)t1ONa407*qoM6N<$f(6GM{r~^~ literal 0 HcmV?d00001 diff --git a/gui/icons/grass/cross.svg b/gui/icons/grass/cross.svg new file mode 100644 index 00000000000..d5e51700a01 --- /dev/null +++ b/gui/icons/grass/cross.svg @@ -0,0 +1,52 @@ + + + + + + + + + + diff --git a/gui/icons/grass/exclamation-mark.png b/gui/icons/grass/exclamation-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..fa17285bd6aa53da211e8c89fc039b50c239cfc8 GIT binary patch literal 492 zcmV)}uwY?yWUAK+4u|?`14Q3FX5h7+es465+2X_FStl>TMj>e?ld{{J0e}d|r0lFA zsSx4A1+D4E4UAT}-trTk*7EvKff9*x+e=o$b;mPbwCk4s6ztsHefqxm2#?kaWVQJ8 z6cI-R005>-KQP)$Ur8|I{x|ejfrrF?qh)@!kMEjIpXEW|*UcSA(ieqB0ur-mJH_h6K+2fpU=f~&%Ef9q1WU`*NtXc-p({Wtv3|z(% i*9V6gVD@Pxj{5@hsh~eBi%shQ0000 + + + + + + diff --git a/gui/icons/grass/question-mark.png b/gui/icons/grass/question-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..247a73e0491caae097d54c9d90ca187f9e221f4f GIT binary patch literal 497 zcmVXjOdMx)EXj+vdI1CnOWxx16e-DaL5I&{4mA^V0 z#27b$vkm=FmgU1`x=sV&i<#vB&bhrVQRfMg&bM)0*XwznpZu>4% + + + + + diff --git a/gui/icons/grass/success.png b/gui/icons/grass/success.png new file mode 100644 index 0000000000000000000000000000000000000000..ff2ec980cd730638f4379c6116e0ed2a2688a35c GIT binary patch literal 595 zcmV-Z0<8UsP)c+KPZIt_yMU2_fsupb$-3ue8#I6Ny<+$w{EVG}<(C1&|NsAq!m1lfx&&{T ze`5I0@L%w@*=Ln4g?<13|7Rdlb8(R5X}?zt{~7-CJXQOtyt!ySOpc%zbXGTN{o?sB z@rCggQI7Xg|HRLGpOxj|3xzBA|NlRO`tk-T)eU7^b(gjaBHOI7u1#2ZTizY!pPc_$ zzX<$S-coQyePbstvKl6x)r~@b_}=e+Znoa@tHASP8Y>#57#JXN{fG6UzVk>@%_pnf@^U%Kj{UL+*>@ZN|qA3pIWTyg97Cwo&7w)UCxYjJD|i z=6TO3b2#QH=NB>GYcW;NG5q-d|9=MM&Ba|3XMNr<{Ac{n{FCdy;*RX+B3B&VGyG@# zFMBxZh3dL0S6l%MXQ*s0O_4nm^PKSy%YVi{EdLq)GyIo0>-|P~Q*j!;7($D#ay!Lc zX^)wIaQ_#)ZT?AlOHntWD8@+2YAahb|M9-v#`urr^lhK+FhbFckq&N!*IZ})DP|(W h!xKeFGXnzy0|1cJTc&^tUO@l=002ovPDHLkV1fonA + + + + + diff --git a/gui/icons/grass/time-period.png b/gui/icons/grass/time-period.png new file mode 100644 index 0000000000000000000000000000000000000000..365fb597bd71a932bcda55cfc68aa2747a8fb3bf GIT binary patch literal 733 zcmV<30wVp1P)bjUBqeYPp@5zXZau-CgUuc#{3NTpIO zK@bdz$RS`WpumENJT=DL9UmW`udbJhfaiJZfHZIphyXq?7YeihwSX@oH&ykDwYCgk zs>oU30x++tDPR+@cX4sC9jFtLe&BdwYF68>Umxha3T_kjFw8J z7r+)^r!mG1hMq5tjg9?K)i$7CRqv&qTy46bd8NiC2QFt*xC31ML>qbw2}Nfi6`&n9XLV2L=Wi6d6=3jsSi0it~fjvN- zwRVO7faiH};0y4|h)AB*B{^2L6aPdimD(IWd%_U&)ni=neZSLjoHONe`QGH@@J0X9cv*&r+MPyJ_yMgBL`@O2>RrOA2c@4h-BJ2%3?EJ`e P00000NkvXXu0mjfp1ejG literal 0 HcmV?d00001 diff --git a/gui/icons/grass/time-period.svg b/gui/icons/grass/time-period.svg new file mode 100644 index 00000000000..48efd0d0879 --- /dev/null +++ b/gui/icons/grass/time-period.svg @@ -0,0 +1,39 @@ + + + + + + diff --git a/gui/wxpython/core/gconsole.py b/gui/wxpython/core/gconsole.py index cb26b57f0f5..4705d639297 100644 --- a/gui/wxpython/core/gconsole.py +++ b/gui/wxpython/core/gconsole.py @@ -41,6 +41,7 @@ from grass.pydispatch.signal import Signal from grass.grassdb import history +from grass.grassdb.history import Status from core import globalvar from core.gcmd import CommandThread, GError, GException @@ -446,6 +447,26 @@ def WriteError(self, text): """Write message in error style""" self.writeError.emit(text=text) + def UpdateHistory(self, status, runtime=None): + """Update command history. + :param enum status: status of command run + :param int runtime: duration of command run + """ + if runtime: + cmd_info = {"runtime": runtime, "status": status.value} + else: + cmd_info = {"status": status.value} + try: + history_path = history.get_current_mapset_gui_history_path() + history.update_entry(history_path, cmd_info) + + # update history model + if self._giface: + entry = history.read(history_path)[-1] + self._giface.entryInHistoryUpdated.emit(entry=entry) + except (OSError, ValueError) as e: + GError(str(e)) + def RunCmd( self, command, @@ -593,6 +614,7 @@ def RunCmd( GUI( parent=self._guiparent, giface=self._giface ).ParseCommand(command) + self.UpdateHistory(status=Status.SUCCESS) except GException as e: print(e, file=sys.stderr) @@ -662,6 +684,7 @@ def RunCmd( if task: # process GRASS command without argument GUI(parent=self._guiparent, giface=self._giface).ParseCommand(command) + self.UpdateHistory(status=Status.SUCCESS) else: self.cmdThread.RunCmd( command, @@ -736,29 +759,18 @@ def OnCmdDone(self, event): ) ) msg = _("Command aborted") - status = "aborted" + status = Status.ABORTED elif event.returncode != 0: msg = _("Command ended with non-zero return code {returncode}").format( returncode=event.returncode ) - status = "failed" + status = Status.FAILED else: msg = _("Command finished") - status = "success" - - cmd_info = {"runtime": int(ctime), "status": status} + status = Status.SUCCESS # update command history log by status and runtime duration - try: - history_path = history.get_current_mapset_gui_history_path() - history.update_entry(history_path, cmd_info) - - # update history model - if self._giface: - entry = history.read(history_path)[-1] - self._giface.entryInHistoryUpdated.emit(entry=entry) - except (OSError, ValueError) as e: - GError(str(e)) + self.UpdateHistory(status=status, runtime=int(ctime)) self.WriteCmdLog( "(%s) %s (%s)" % (str(time.ctime()), msg, stime), diff --git a/gui/wxpython/history/tree.py b/gui/wxpython/history/tree.py index caaeb8bda86..ead959fbaae 100644 --- a/gui/wxpython/history/tree.py +++ b/gui/wxpython/history/tree.py @@ -36,10 +36,17 @@ from gui_core.treeview import CTreeView from gui_core.wrap import Menu +from icons.icon import MetaIcon + from grass.pydispatch.signal import Signal from grass.grassdb import history +from grass.grassdb.history import Status + +# global variables for node types +TIME_PERIOD = "time_period" +COMMAND = "command" # global variable for purposes of sorting "No time info" node OLD_DATE = datetime.datetime(1950, 1, 1).date() @@ -108,6 +115,17 @@ def __init__( self._resetSelectVariables() + self._iconTypes = [ + TIME_PERIOD, + Status.ABORTED.value, + Status.FAILED.value, + Status.RUNNING.value, + Status.SUCCESS.value, + Status.UNKNOWN.value, + ] + + self._initImages() + self._initHistoryModel() self.showNotification = Signal("HistoryBrowserTree.showNotification") @@ -144,6 +162,21 @@ def _resetSelectVariables(self): self.selected_day = [] self.selected_command = [] + def _initImages(self): + bmpsize = (16, 16) + icons = { + TIME_PERIOD: MetaIcon(img="time-period").GetBitmap(bmpsize), + Status.ABORTED.value: MetaIcon(img="exclamation-mark").GetBitmap(bmpsize), + Status.FAILED.value: MetaIcon(img="cross").GetBitmap(bmpsize), + Status.RUNNING.value: MetaIcon(img="circle").GetBitmap(bmpsize), + Status.SUCCESS.value: MetaIcon(img="success").GetBitmap(bmpsize), + Status.UNKNOWN.value: MetaIcon(img="question-mark").GetBitmap(bmpsize), + } + il = wx.ImageList(bmpsize[0], bmpsize[1], mask=False) + for each in self._iconTypes: + il.Add(icons[each]) + self.AssignImageList(il) + def _confirmDialog(self, question, title): """Confirm dialog""" dlg = wx.MessageDialog(self, question, title, wx.YES_NO) @@ -200,14 +233,14 @@ def _initHistoryModel(self): day = self._model.SearchNodes( parent=self._model.root, day=self._timestampToDay(timestamp), - type="day", + type=TIME_PERIOD, ) else: # Find day node prepared for entries without any command info day = self._model.SearchNodes( parent=self._model.root, day=self._timestampToDay(), - type="day", + type=TIME_PERIOD, ) if day: @@ -218,13 +251,13 @@ def _initHistoryModel(self): # Prepare it for entries without command info day = self._model.AppendNode( parent=self._model.root, - data=dict(type="day", day=self._timestampToDay()), + data=dict(type=TIME_PERIOD, day=self._timestampToDay()), ) else: day = self._model.AppendNode( parent=self._model.root, data=dict( - type="day", + type=TIME_PERIOD, day=self._timestampToDay( entry["command_info"]["timestamp"] ), @@ -236,14 +269,14 @@ def _initHistoryModel(self): entry["command_info"].get("status") if entry.get("command_info") and entry["command_info"].get("status") is not None - else "unknown" + else Status.UNKNOWN.value ) # Add command to time period node self._model.AppendNode( parent=day, data=dict( - type="command", + type=COMMAND, name=entry["command"].strip(), timestamp=timestamp if timestamp else None, status=status, @@ -306,10 +339,10 @@ def DefineItems(self, selected): self._resetSelectVariables() for item in selected: type = item.data["type"] - if type == "command": + if type == COMMAND: self.selected_command.append(item) self.selected_day.append(item.parent) - elif type == "day": + elif type == TIME_PERIOD: self.selected_command.append(None) self.selected_day.append(item) @@ -323,7 +356,7 @@ def Filter(self, text): except re.error: return self._model = self._orig_model.Filtered( - method="filtering", name=compiled, type="command" + method="filtering", name=compiled, type=COMMAND ) else: self._model = self._orig_model @@ -344,13 +377,13 @@ def InsertCommand(self, entry): # Check if today time period node exists or create it today = self._timestampToDay(entry["command_info"]["timestamp"]) today_nodes = self._model.SearchNodes( - parent=self._model.root, day=today, type="day" + parent=self._model.root, day=today, type=TIME_PERIOD ) if not today_nodes: today_node = self._model.AppendNode( parent=self._model.root, data=dict( - type="day", + type=TIME_PERIOD, day=today, ), ) @@ -361,10 +394,10 @@ def InsertCommand(self, entry): command_node = self._model.AppendNode( parent=today_node, data=dict( - type="command", + type=COMMAND, name=entry["command"].strip(), timestamp=entry["command_info"]["timestamp"], - status=entry["command_info"].get("status", "in process"), + status=entry["command_info"].get("status", Status.UNKNOWN.value), ), ) @@ -386,9 +419,9 @@ def UpdateCommand(self, entry): # Get node of last command today = self._timestampToDay(entry["command_info"]["timestamp"]) today_node = self._model.SearchNodes( - parent=self._model.root, day=today, type="day" + parent=self._model.root, day=today, type=TIME_PERIOD )[0] - command_nodes = self._model.SearchNodes(parent=today_node, type="command") + command_nodes = self._model.SearchNodes(parent=today_node, type=COMMAND) last_node = command_nodes[-1] # Remove last node @@ -433,6 +466,17 @@ def Run(self, node=None): showTraceback=False, ) + def OnGetItemImage(self, index, which=wx.TreeItemIcon_Normal, column=0): + """Overridden method to return image for each item.""" + node = self._model.GetNodeByIndex(index) + try: + if node.data["type"] == TIME_PERIOD: + return self._iconTypes.index(node.data["type"]) + elif node.data["type"] == COMMAND: + return self._iconTypes.index(node.data["status"]) + except ValueError: + return 0 + def OnRemoveCmd(self, event): """Remove cmd from the history file""" self.DefineItems(self.GetSelected()) diff --git a/gui/wxpython/lmgr/frame.py b/gui/wxpython/lmgr/frame.py index c24ee04ab8e..819a80ffb95 100644 --- a/gui/wxpython/lmgr/frame.py +++ b/gui/wxpython/lmgr/frame.py @@ -83,6 +83,7 @@ create_location_interactively, ) from grass.grassdb.checks import is_first_time_user +from grass.grassdb.history import Status class GMFrame(wx.Frame): @@ -947,8 +948,9 @@ def _switchPage(self, notification): def RunSpecialCmd(self, command): """Run command from command line, check for GUI wrappers""" + result = True if re.compile(r"^d\..*").search(command[0]): - self.RunDisplayCmd(command) + result = self.RunDisplayCmd(command) elif re.compile(r"r[3]?\.mapcalc").search(command[0]): self.OnMapCalculator(event=None, cmd=command) elif command[0] == "i.group": @@ -968,15 +970,21 @@ def RunSpecialCmd(self, command): elif command[0] == "cd": self.OnChangeCWD(event=None, cmd=command) else: + result = False raise ValueError( "Layer Manager special command (%s)" " not supported." % " ".join(command) ) + if result: + self._gconsole.UpdateHistory(status=Status.SUCCESS) + else: + self._gconsole.UpdateHistory(status=Status.FAILED) def RunDisplayCmd(self, command): """Handles display commands. :param command: command in a list + :return int: False if failed, True if succcess """ if not self.currentPage: self.NewDisplay(show=True) @@ -984,7 +992,7 @@ def RunDisplayCmd(self, command): if command[0] == "d.erase": # rest of d.erase is ignored self.GetLayerTree().DeleteAllLayers() - return + return False try: # display GRASS commands layertype = command2ltype[command[0]] @@ -997,7 +1005,7 @@ def RunDisplayCmd(self, command): ) % command[0], ) - return + return False if layertype == "barscale": if len(command) > 1: @@ -1051,6 +1059,7 @@ def RunDisplayCmd(self, command): lname=lname, lcmd=command, ) + return True def GetLayerNotebook(self): """Get Layers Notebook""" diff --git a/gui/wxpython/main_window/frame.py b/gui/wxpython/main_window/frame.py index 3f005defc18..3ffedcb7f4d 100644 --- a/gui/wxpython/main_window/frame.py +++ b/gui/wxpython/main_window/frame.py @@ -89,6 +89,7 @@ create_location_interactively, ) from grass.grassdb.checks import is_first_time_user +from grass.grassdb.history import Status class SingleWindowAuiManager(aui.AuiManager): @@ -1084,8 +1085,9 @@ def FocusPage(self, page_text): def RunSpecialCmd(self, command): """Run command from command line, check for GUI wrappers""" + result = True if re.compile(r"^d\..*").search(command[0]): - self.RunDisplayCmd(command) + result = self.RunDisplayCmd(command) elif re.compile(r"r[3]?\.mapcalc").search(command[0]): self.OnMapCalculator(event=None, cmd=command) elif command[0] == "i.group": @@ -1105,15 +1107,21 @@ def RunSpecialCmd(self, command): elif command[0] == "cd": self.OnChangeCWD(event=None, cmd=command) else: + result = False raise ValueError( "Layer Manager special command (%s)" " not supported." % " ".join(command) ) + if result: + self._gconsole.UpdateHistory(status=Status.SUCCESS) + else: + self._gconsole.UpdateHistory(status=Status.FAILED) def RunDisplayCmd(self, command): """Handles display commands. :param command: command in a list + :return int: False if failed, True if succcess """ if not self.currentPage: self.NewDisplay(show=True) @@ -1121,7 +1129,7 @@ def RunDisplayCmd(self, command): if command[0] == "d.erase": # rest of d.erase is ignored self.GetLayerTree().DeleteAllLayers() - return + return False try: # display GRASS commands layertype = command2ltype[command[0]] @@ -1134,7 +1142,7 @@ def RunDisplayCmd(self, command): ) % command[0], ) - return + return False if layertype == "barscale": if len(command) > 1: @@ -1188,6 +1196,7 @@ def RunDisplayCmd(self, command): lname=lname, lcmd=command, ) + return True def GetAuiManager(self): """Get aui manager diff --git a/python/grass/grassdb/history.py b/python/grass/grassdb/history.py index b1ddb52c19a..d20628dc820 100644 --- a/python/grass/grassdb/history.py +++ b/python/grass/grassdb/history.py @@ -11,6 +11,7 @@ import json import shutil +from enum import Enum from pathlib import Path from datetime import datetime @@ -18,6 +19,17 @@ from grass.script.utils import parse_key_val +class Status(Enum): + """Enum representing a set of status constants + that are used to represent various states or command outcomes.""" + + ABORTED = "aborted" + FAILED = "failed" + RUNNING = "running" + SUCCESS = "success" + UNKNOWN = "unknown" + + def get_current_mapset_gui_history_path(): """Return path to the current mapset history file. This function does not ensure that the file exists. @@ -278,6 +290,7 @@ def get_initial_command_info(env_run): "mask2d": mask2d_present, "mask3d": mask3d_present, "region": region_settings, + "status": Status.RUNNING.value, } return cmd_info